zustand-nibble
v0.2.5
Published
Composable zustand stores with nibbles
Downloads
4
Readme
zustand-nibble
Split a zustand store into smaller pieces, called nibbles. Compared to slices which are spread at the top-level of the store, nibbles can be placed anywhere in the parent store.
import { type StateCreator, create } from 'zustand';
import nibble from 'zustand-nibble';
export interface Child {
name: string;
age: number;
birthday: () => void;
}
export interface Parent {
name: string;
age: number;
child: Child;
birthday: () => void;
}
const createJoe: StateCreator<Child> = set => ({
name: 'Joe Doe',
age: 10,
birthday: () => set(state => ({ age: state.age + 1 })),
});
const useParent = create<Parent>()((set, get, api) => ({
name: 'John Doe',
age: 42,
child: nibble(api)(state => state.child)(createJoe),
birthday: () => set(state => ({ age: state.age + 1 })),
}));
nibble(api)(getter)
receives the following arguments:
- The parent store
api
. - A
getter
that extracts the child state from the parent state.
It returns a function that accepts a StateCreator
to create the child state, similar to a zustand middleware.
Installation
npm
npm install zustand-nibble
bun
bun install zustand-nibble
Why not use Immer?
immer
and zustand-nibble
both simplify nested state updates.
I would argue, that immer is the better choice here. In fact, zustand-nibble uses immer under the hood to update the parent state.
The primary use of a nibble is to decouple the child state from the parent state. This allows the composition of independent states into any structure, even dynamically.
In the example above, createJoe
is independent of the parent state. It can be integrated in any store that accepts a Child
, using a nibble to link them together. This decoupling in not possible with immer alone, as it always operates on the parent state.
Naturally, immer
and zustand-nibble
can be used together.
Use as Recipe
The function returned by nibble
can be used as a recipe in multiple stores.
// Omit the api to create a recipe
const createChild = nibble<Parent>()(state => state.child); // Recipe<Parent, Child>
const useDad = create<Parent>()((set, get, api) => ({
name: 'John Doe',
age: 42,
child: createChild(api, createJoe), // call recipe
//...
}));
const useMom = create<Parent>()((set, get, api) => ({
name: 'Jane Doe',
age: 37,
child: createChild(api, createJoe), // call recipe
//...
}));
/* Note that the childs are separate instances.
There is no state sharing through nibbles */
Create a Store API
To create a store API for the child state, simply omit the state creator.
const createChild = nibble<Parent>()(state => state.child); // Recipe<Parent, Child>
const parentStore = createStore<Parent>()((set, get, api) => ({
name: 'John Doe',
age: 42,
child: createChild(api, createJoe), // Child
//...
}));
const childStore = createChild(parentStore); // StoreApi<Child>
Arrays
Arrays are objects in JavaScript, by default the setter will merge the array using Object.assign
. This is equvialent to how zustand handles array states.
Likewise, you can use the replace
flag to disable this merging behavior.
childStore.setState([4, 5]); // [1, 2, 3] -> [4, 5, 3]
childStore.setState([4, 5], /*replace*/ true); // [1, 2, 3] -> [4, 5]
Tip: If possible wrap the array in an object and instead use that object as the root of your state. This applies to both zustand and zustand-nibble.
// Instead of this:
const useNumbers = create<number[]>(...); // zustand
nibble<number[]>()(getter); // zustand-nibble
// Do this:
interface State {
values: number[];
}
const useNumbers = create<State>(...); // zustand
nibble<State>()(getter); // zustand-nibble