In another epiphany of the morning of this December 31, I enhanced my previous utility function to create Zustand stores, including devtools
and immer
middlewares:
import {create, StateCreator, StoreApi, UseBoundStore} from 'zustand';
import {devtools} from 'zustand/middleware';
import {immer} from 'zustand/middleware/immer';
type ZustandActions<T extends object, U extends object> = StateCreator<
T,
[['zustand/devtools', never], ['zustand/immer', never]],
[['zustand/immer', never], ['zustand/devtools', never]],
U
>;
/**
* Creates a Zustand store with state, computed and actions.
*/
export function createStore<
T extends object,
U extends object,
C extends Record<string, (s: UseBoundStore<StoreApi<T>>) => void>,
> (
{
state,
computed = {} as C,
actions = {} as ZustandActions<T, U>,
}: {
state: T;
computed?: C;
actions?: ZustandActions<T, U>;
},
) {
const store = create(
devtools(
immer(() => state),
{name: Math.random().toString(36).slice(2), serialize: true},
),
);
if (import.meta.env.DEV) {
for (const name in computed) {
if (!name.startsWith('use'))
throw new Error(name + ' is a hook, and its name must start with the word "use".');
}
}
return {
use: store,
...computed,
...actions(store.setState, store.getState, store),
};
}
The key improvement here is that computed
functions will be 100% reactive, because they’re simple hooks attached to the store object – they work like ordinary hook functions:
const bearStore = createStore({
state: {
bears: 0,
},
computed: {
useHasBears: () => bearStore.use(s => s.bears > 0),
},
actions: (set, get) => ({
increasePopulation(): void {
set(state => {
++state.bears;
});
},
}),
});
function App() {
const bears = bearStore.use(s => s.bears);
const hasBears = bearStore.useHasBears();
return <div>
<span>{bears} - {hasBears}</span>
<button onClick={() => bearStore.increasePopulation()}>
Change
</button>
</div>;
}
I’m in awe looking at such a small function yielding such a great usability.