Tuesday, December 31, 2024

Zustand, devtools and immer wrapper, pt. 3

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.

No comments: