Development··3 min read

Why I Switched from Zustand to Jotai

The reasons behind migrating state management libraries, the process, and what changed

I Didn't Start Hating Zustand

Let me be clear up front. Zustand is a great library. I used it happily for over two years. I didn't switch because of some critical flaw. But as the project grew, certain aspects of Zustand's approach started not fitting as well.

That friction was at the "maybe I should try something else" level, but it ended up turning into a full migration.

The Stores Were Getting Fat

Zustand's default pattern is grouping related state into a single store: useUserStore, useCartStore, and so on. Clean at first. But as features piled on, a single store ballooned to 20, 30 pieces of state.

useUserStore ended up holding user info, auth tokens, profile editing state, notification settings, and recent searches. Change any one piece and every component subscribing to that store becomes a re-render candidate. Selectors let you subscribe to only what you need, but having to think about selectors every time was cognitive overhead. (I've caught performance issues from forgotten selectors at least three times.)

Jotai Takes a Different Approach

Instead of big stores, state gets split into the smallest possible units (atoms). userAtom, authTokenAtom, notificationSettingsAtom are each independent.

Where the difference becomes tangible is re-render scope. When notification settings change, components subscribing to user info don't re-render. Granular subscriptions happen automatically, without selectors. "Granular by default" was the single biggest reason I chose Jotai.

Derived State Is Elegant

In Zustand, creating derived state means using get() inside the store or computing in selectors. In Jotai, you compose atoms: atom((get) => get(cartAtom).length). Cart item count, total price, discounted price -- each becomes its own atom. When the source changes, only dependent atoms recompute, and only subscribing components re-render. This automatic dependency tracking is genuinely pleasant to work with.

Migration Took 3 Days

One store per day, converted to atoms. The most time-consuming part was async state. In Zustand, you just use async/await inside actions. Jotai has a separate concept: async atoms via atom(async (get) => { ... }). The way it integrates with Suspense felt unfamiliar at first.

Bundle Size Was an Unexpected Win

Zustand minified is about 3.5KB, Jotai about 3.2KB. Not a huge gap, but Jotai's import-only-what-you-use structure means better tree-shaking. In the actual bundle, Jotai was about 1KB smaller. Not a user-facing difference, but in the spirit of "sweat the small stuff." (This is slightly self-rationalizing, I'll admit.)

There Are Things I Miss, Honestly

DevTools are weaker in Jotai. Zustand integrates smoothly with Redux DevTools for visual state tracking. Jotai's DevTools haven't reached that level yet.

And when atoms get too granular, you end up with a lot of files. Once the atoms/ folder passed 20-something files, it created its own management overhead. I solved it by establishing a convention of grouping related atoms in single files, but I should have set that convention from the start -- cleaning up later cost me time.

Your Project Decides Which to Use

Having used both: if your state is simple and small, Zustand has a lower barrier to entry. If your state is complex with lots of interdependencies, Jotai's atom model shines. "Which library is better" isn't the question -- it's "what does my project's state structure look like" that determines the answer. Fit the tool to the project, not the project to the tool. But I'll admit -- even knowing this, I sometimes pick the tool first. I'm only human.

Related Posts