Thursday, June 8, 2023

Using Zustand, TypeScript and Immer together

In React land, Redux nightmare finally came to an end with the inception of Zustand, which throws away all the chores we had to previously do, leaving us with a simple workflow of minimal code. On top of that, TypeScript allows us to relax our brains with some degree of strong typing.

When dealing with our Zustand store, just like Redux, we have to manipulate a chunk of immutable data, which becomes more and more annoying as we deepen the object structure. And, when it comes to dealing immutable data, Immer is everyone’s best friend – just slap a produce call and you can go home earlier.

Now here comes the question: how to use Immer inside our Zustand store?

Turns out Zustand provides a few middlewares – functions which lie between the data and your manipulation functions –, and among them an Immer middleware. Using the middleware has two advantages:

  • you don’t need to explicitly call produce, because the state argument already comes to you as a WritableDraft;
  • you don’t need to explicitly type the state argument, because it already comes to you properly typed.

To put everything together, with automatic type inference, we also need the combine middleware, so we don’t need to write the store type by hand. Here is the full template that can be used as the starting point of your store:

import {create} from 'zustand';
import {combine} from 'zustand/middleware';
import {immer} from 'zustand/middleware/immer';

interface Person {
	name: string;
	age: number;
}

const useStore = create(immer(
	combine({
		people: [] as Person[],
	},
	(set, get) => ({
		add(name: string, age: number) {
			set(state => {
				state.people.push({name, age});
			});
		},
		remove(name: string) {
			set(state => {
				state.people = state.people.filter(p => p.name === name);
			});
		},
	})),
));

export default useStore;

Notice how you can simply mutate the state inside the actions.

No comments: