pretty-good-state
v2.2.2
Published
A just-enough state management library for React. Built on top of [valtio](https://github.com/pmndrs/valtio).
Readme
pretty-good-state
A just-enough state management library for React. Built on top of valtio.
npm install pretty-good-state✅ Fine-grained reactivity
✅ Simple and intuitive mutations
✅ Unified API for local, global, and context state
✅ Full TypeScript support
Usage
Creating State
Use the defineState() function to create reusable state:
import { defineState } from "pretty-good-state";
const CounterState = defineState({
count: 0,
});You can also define methods on the state that directly mutate it:
const CounterState = defineState({
count: 0,
increment(amount = 1) {
// `this` is bound to the state
this.count += amount;
},
});Local State
Use useLocalState() to initialize component-local state:
import { useLocalState } from "pretty-good-state";
function Counter() {
const counter = useLocalState(CounterState);
return (
<div>
<p>Count: {counter.count}</p>
<button onClick={counter.increment}>Increment</button>
</div>
);
}You can also configure the initial state:
const counter = useLocalState(CounterState, (state) => {
state.count = 10;
});Shared State
Use useProvidedState() with a Provider to share state for a portion
of your React tree:
import { useProvidedState } from "pretty-good-state";
function Counter() {
const counter = useProvidedState(CounterState);
return (
<div>
<p>Count: {counter.count}</p>
<button onClick={counter.increment}>Increment</button>
</div>
);
}
function Page() {
return (
<CounterState.Provider>
{/* The following counters will share the same state */}
<Counter />
<Counter />
</CounterState.Provider>
);
}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:
import { useLocalState } from "pretty-good-state";
function Page() {
const counter = useLocalState(CounterState);
return (
<CounterState.Provider state={counter}>
<Counter />
<button onClick={() => counter.increment(10)}>Increment 10</button>
</CounterState.Provider>
);
}Mutating State
In addition to defining state methods, you can also directly modify the state in components:
import { useLocalState } from "pretty-good-state";
const counter = useLocalState(CounterState);
<button onClick={() => (counter.count = 0)}>Reset</button>;The library detects mutations and re-renders the components that depend on those exact changes.
Passing State to Child Components
You can directly pass state to child components:
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 (
<div>
{todoList.items.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
}
function TodoItem({ todo }: { todo: Todo }) {
return (
<div>
<input
type="checkbox"
checked={todo.done}
onChange={() => {
todo.done = !todo.done;
}}
/>
<span>{todo.text}</span>
</div>
);
}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:
import { usePassedState } from "pretty-good-state";
function TodoItem({ todo: _todo }: { todo: Todo }) {
const todo = usePassedState(_todo);
return (
<div>
<input
type="checkbox"
checked={todo.done}
onChange={() => {
todo.done = !todo.done;
}}
/>
<span>{todo.text}</span>
</div>
);
}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.
Accessing Global State Outside of a Component
The globalStore object lets you access global state outside of a component:
import { globalStore } from "pretty-good-state";
const counter = globalStore.getState(CounterState);
counter.increment();Accessing Hooks in State
There may be cases where you want to have access to hooks from within a state.
The runInComponent() function lets you do this.
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).
TypeScript Types
You can infer the types of the state from its constructor:
import { defineState, Infer } from "pretty-good-state";
const CounterState = defineState({
count: 0,
});
type CounterShape = Infer<typeof CounterState>; // { count: number }