A just-enough state management library for React. Built on top of [valtio](https://github.com/pmndrs/valtio).
npm install pretty-good-stateA just-enough state management library for React. Built on top of valtio.
``bash`
npm install pretty-good-state
✅ Fine-grained reactivity
✅ Simple and intuitive mutations
✅ Unified API for local, global, and context state
✅ Full TypeScript support
Use the defineState() function to create reusable state:
`tsx
import { defineState } from "pretty-good-state";
const CounterState = defineState({
count: 0,
});
`
You can also define methods on the state that directly mutate it:
`tsxthis
const CounterState = defineState({
count: 0,
increment(amount = 1) {
// is bound to the state`
this.count += amount;
},
});
Use useLocalState() to initialize component-local state:
`tsx
import { useLocalState } from "pretty-good-state";
function Counter() {
const counter = useLocalState(CounterState);
return (
Count: {counter.count}
You can also configure the initial state:
`tsx
const counter = useLocalState(CounterState, (state) => {
state.count = 10;
});
`$3
Use
useProvidedState() with a Provider to share state for a portion
of your React tree:`tsx
import { useProvidedState } from "pretty-good-state";function Counter() {
const counter = useProvidedState(CounterState);
return (
Count: {counter.count}
);
}function Page() {
return (
{/ The following counters will share the same state /}
);
}
`You can also call
useProvidedState() without a Provider, in which case it will
use a shared global state.If you pass a state object to the Provider, it will use that state object
instead of creating a new one. This is useful when you want to access the state
in the same component that renders the Provider:
`tsx
import { useLocalState } from "pretty-good-state";function Page() {
const counter = useLocalState(CounterState);
return (
);
}
`$3
In addition to defining state methods, you can also directly modify the state in
components:
`tsx
import { useLocalState } from "pretty-good-state";const counter = useLocalState(CounterState);
;
`The library detects mutations and re-renders the components that depend on those
exact changes.
$3
You can directly pass state to child components:
`tsx
import { defineState, useLocalState } from "pretty-good-state";const TodoListState = defineState({
items: [] as Todo[],
});
type Todo = {
id: string;
text: string;
done: boolean;
};
function TodoList() {
const todoList = useLocalState(TodoListState);
return (
{todoList.items.map((todo) => (
))}
);
}function TodoItem({ todo }: { todo: Todo }) {
return (
type="checkbox"
checked={todo.done}
onChange={() => {
todo.done = !todo.done;
}}
/>
{todo.text}
);
}
`However, note that the hook
useLocalState() is in the parent component,
and as a result the parent is tracking changes to all state properties accessed
in the child. The parent re-renders unnecessarily when, for example, todo.done
is toggled.To optimize this, we can use the
usePassedState() hook to allow the child
component to track its own state:`tsx
import { usePassedState } from "pretty-good-state";function TodoItem({ todo: _todo }: { todo: Todo }) {
const todo = usePassedState(_todo);
return (
type="checkbox"
checked={todo.done}
onChange={() => {
todo.done = !todo.done;
}}
/>
{todo.text}
);
}
`Notice how we rename
todo to _todo as we destructure the component props.
This helps to avoid accidental reads from the parent's copy of the state.$3
The
globalStore object lets you access global state outside of a component:`tsx
import { globalStore } from "pretty-good-state";const counter = globalStore.getState(CounterState);
counter.increment();
`$3
There may be cases where you want to have access to hooks from within a state.
The
runInComponent() function lets you do this.`tsx
import { defineState, runInComponent } from "pretty-good-state";const EmailFormState = defineState({
getIntl: runInComponent(() => {
return useIntl();
}),
email: "",
errorMessage: "",
validate() {
if (!this.email) {
this.errorMessage = this.getIntl().format("Email is required");
return false;
}
this.errorMessage = "";
return true;
},
});
`These
runInComponent() functions are called in the component where the local
state is created – i.e. when useLocalState() is called or when a Provider is
rendered.Note that since
runInComponent() requires a component context, it cannot be
used in global state (i.e.
globalStore.getState(EmailFormState).validate() will throw an error).$3
You can infer the types of the state from its constructor:
`tsx
import { defineState, Infer } from "pretty-good-state";const CounterState = defineState({
count: 0,
});
type CounterShape = Infer; // { count: number }
``