On further researching of my previous idea of creating a Zustand + Immer wrapper, I found a very interesting technique straight from Zustand docs, where all the methods are simply free functions declared outside the store, called no store actions. I was already using this to declare private store methods, but it never occurred me to use it for all methods.
Since it officially has no downsides, I applied this idea to append the actions directly to the hook itself, so we don’t need to import each free function. The problem is that the hook has a couple methods itself, plus the inherent ones to any function object. So the result is very polluted. So I decided to adopt a convention of prefixing every action with $
.
But conventions are easy to break. So I implemented a reduce
to prefix each method name with #
. But then TypeScript could not infer the type anymore, so I had to dig and type it manually with key remapping, and finally I got this implementation:
import {create, StateCreator} from 'zustand'; import {devtools} from 'zustand/middleware'; import {immer} from 'zustand/middleware/immer'; export function createStore<T extends object, U extends object>( initialState: T, actions: StateCreator< T, [['zustand/devtools', never], ['zustand/immer', never]], [['zustand/immer', never], ['zustand/devtools', never]], U >, ) { const store = create( devtools( immer(() => initialState), {name: Math.random().toString(36).slice(2), serialize: true}, ), ); type WithPrefix<T> = { [K in keyof T as K extends string ? `$${K}` : never]: T[K]; }; const actionsOrig = actions(store.setState, store.getState, store); const actionsPrefixed = Object.entries(actionsOrig).reduce( (accum, [k, v]) => ({...accum, ['$' + k]: v}), {} as WithPrefix<typeof actionsOrig>, ); return Object.assign(store, actionsPrefixed); }
This creator allows us to call actions directly, without the hook. They’ll be automatically prefixed with $
:
const useFoo = createStore('Foo', { name: '', }, (set, get) => ({ setName(name: string): void { set(state => { state.name = name; }); }, })); function App() { const name = useFoo(s => s.name); return <div> <span>{name}</span> <button onClick={() => useFoo.$setName('Joe')}</button> Change </button> </div>; }
That $
prefix gives me bad Vue vibes. Let’s see if I can get along with it, because I was able to develop a DX which is exactly what I was looking for.