✳ Nairobi, Kenya
- WhatsApp+254 799 756 331
- Emailhello@muvevi.com
- LinkedInlinkedin.com/in/muvevi
- GitHubgithub.com/mutua-muvevi
- Twitter / X@muvevi
Available for freelance & full-time roles.
Let's build something great.
TypeScript + Zustand v5
— the patterns that work at scale
Zustand is deceptively simple. At scale, the lack of opinions becomes a problem. Here's the patterns I landed on after building 10+ stores.
Zustand is the best state management library for React, but it has one footgun that'll wreck you at scale: the getSnapshot infinite loop.
In Zustand v5, the selector passed to useStore must be referentially stable between renders. If you call a function inside the selector — even a method on the store itself — you create a new reference every render, which triggers a re-subscribe, which triggers another render, and so on.
// THIS CAUSES AN INFINITE LOOP
const tasks = useTasksStore((s) => s.getByProject(projectId));
// CORRECT — select the raw array, filter in component body
const allTasks = useTasksStore((s) => s.tasks);
const tasks = allTasks.filter((t) => t.projectId === projectId);This is the single most important thing to know about Zustand v5. Everything else is structure.
Store shape. I organize stores around domain entities, not UI state. Each store owns one type's CRUD and nothing about how it's displayed. The type definitions live in a separate *.types.ts file so they can be imported by both the store and the components without creating circular dependencies.
store/
├── tasks/
│ ├── tasks.types.ts ← types, no imports from store
│ ├── tasks.store.ts ← create(), persist()
│ └── index.ts ← re-exports both
└── index.ts ← re-exports all storesPersist with name namespacing. Every persisted store has a unique name string. I prefix with the app name: "muvevi-tasks", "muvevi-finances". Without namespacing, two different stores that both use "tasks" will collide in localStorage if the user opens two apps in the same browser.
Computed values outside the store. useMemo in the component, not get() calls in the store. The store's job is to hold data and expose mutations. Derivations belong at the component level. This keeps the store lean and avoids the selector instability issue above.
TypeScript inference. Don't type the store as StoreApi<T> manually. Instead, define the state type and let TypeScript infer the store from create<YourState>(). The only place to reach for explicit typing is when you need to type the immer-style set function in devtools.