Zustand, the middleground between context and redux

Zustand is one of those new state management libraries. Invented well after the rise and pleatu of redux, invented well after the easification of Context, invented in a time where there not enough good options.

When Context came out, we learned about tree local state. I thought Context was perfect for tree local state. Overtime, after practicing with it for a while (see: years), you could use it for tree local state. However, it had its share of downsides: poor performance characteristics and developer experience issues.

Tree local state is better answered by Zustand (and its friends, of course: recoil, jotai).

What does that term even mean, tree local state? In a component, you have a useState hook. Whatever that state is, it is local state to that component. You could use it to render some elements, you could use it in an event handler, perhaps dangerously use it in a side effect, or hey, you could even pass it down to another component as a prop. So maybe you can call that local state and everyone would know roughly what you mean.

What if you wanted some state that’s shared by a group of components? Since React represents everything internally as a tree, maybe you could extend our local state concept and say that higher level state, that shared state, is tree local state. It’s one step below global state, but no less useful.

Zustand does that!

One of my favorite features of Zustand is the decoupling between interacting with tree local state and interacting with components. With redux, you can weasel your way out of having to do everything inside of components. That’s great. With Context, on the other hand, it is a lot more difficult or impossible. Zustand, having historical scaffolding, build their API for both use cases, either in-component or out-of-component. It’s pretty clever.

Example

Here’s my decoupling example.

interface Video {
  videoId: string;
}

interface VideoStoreState {
  videos: Video[];
  addVideo: (video: Video) => void;
}

const videoStore = createVanilla<VideoStoreState>((set) => ({
  videos: [],
  addVideo: (video: Video) =>
    set((state) => ({ videos: [...state.videos, video] })),
}));

const useVideoStore = create(videoStore);

You can use Zustand without React! You can create a store outside of React’s <App /> and it can just work. I can admit this now, it is effectively Global State in this case, but you can treat it as if it were tree local. You might not need so much rigor.

You can define the state the store contains, and the actions you can mutate the store with.

Now, with some out-of-react side effect that your application might have, you can trigger state changes. Let’s say that’s a ws connection, and someone has added a video. Maybe that’s like this:

// hypothentical out of band event handler, possibly coming from a `ws`
window.addEvent("add-new-video", (event) => {
    videoStore.getState().addVideo({videoId: event.params.videoId});
});

Then using the useVideoStore hook, you can connect the out-react-state right back into React, easily.

export function DisplayCase() {
    // slice the state to get the videos
    const videos = useVideoStore((state) => state.videos);
    return videos.map(video => <VideoDisplay key={video.videoId} id={video.videoId} />);
}

Now, it’s not as rigorous as Redux or RTK. And it’s not as annoying as Context and making all these bells and whistles yourself. It’s a perfectly fine middleground.

Give it a try.

P.S. give Immer a try

Follow me on Mastodon @ryanmr@mastodon.cloud.

Follow me on Twitter @ryanmr.