🧠 Redux Toolkit vs Zustand vs Jotai — A Complete Guide to Choosing the Right State Management for React
"State is the memory of your application. How you manage it determines whether your app scales gracefully or collapses under its own weight."
As your React application grows beyond a handful of components, local state stops being enough. You start wrestling with shared data across screens, async API calls, caching, global UI flags, and predictable debugging. That's when you reach for a state management library — and immediately face the hardest decision: which one?
Redux Toolkit, Zustand, and Jotai are the three most popular React state management libraries today, and they solve the same fundamental problem in radically different ways. Picking the wrong one can mean months of refactoring. Picking the right one can save your team hundreds of hours.
This guide covers what each library does, why their mental models differ, when to choose which, performance and re-render behavior, middleware and DevTools, TypeScript and SSR support, scalability for teams, and the decision frameworks every React developer should know.
📚 Table of Contents
- Why This Comparison Matters
- Mental Models The Core Difference
- High Level Architecture
- Redux Toolkit RTK Overview
- Zustand Overview
- Jotai Overview
- Side by Side Feature Comparison
- Performance and Re render Behavior
- Middleware and DevTools
- TypeScript Support
- SSR and Next.js Compatibility
- Scalability and Team Collaboration
- How to Choose Decision Frameworks
- Real World Use Case Mapping
- Migration Guidance
- Best Practices
- State Management Checklist
- Key Interview Takeaways
- Further Reading and Resources
🔹 Why This Comparison Matters
The State Management Problem
Every non-trivial React application eventually faces these challenges:
| Challenge | What Happens Without Proper State Management |
|---|---|
| Shared data across screens | Props drilled 6+ levels deep; components tightly coupled |
| Async API calls | Loading/error states scattered across components; race conditions |
| Caching & deduplication | Same API called multiple times; stale data shown to users |
| Global UI state | Modals, toasts, sidebar toggles managed with ad-hoc context |
| Predictable debugging | State changes impossible to trace; "how did this value get here?" |
| Multi-team collaboration | Developers overwriting each other's state; merge conflicts |
Why These Three?
The React state management landscape has dozens of libraries, but Redux Toolkit, Zustand, and Jotai represent the three fundamental paradigms:
| Paradigm | Library | Philosophy |
|---|---|---|
| Flux / Centralized | Redux Toolkit | One global store, dispatched actions, pure reducers |
| Hook-based / Minimal | Zustand | Simple external store consumed via hooks, no ceremony |
| Atomic / Bottom-up | Jotai | Independent state atoms composed together, fine-grained |
Understanding when and why to choose each paradigm is a core frontend architecture skill — and a top-tier interview topic.
🔹 Mental Models The Core Difference
Think of state management like organizing information in different ways:
| Library | Mental Model | Real-World Analogy |
|---|---|---|
| Redux Toolkit | Centralized enterprise data system | A corporate filing system — everything goes through a central office, forms are filled out (actions), processed (reducers), and filed (store) |
| Zustand | Simple global store with hooks | A shared whiteboard — anyone can read it, anyone can update it, no paperwork needed |
| Jotai | Atomic state pieces wired together | Spreadsheet cells — each cell holds one value, and cells can reference (derive from) other cells |
💡 Key Insight: The right choice depends on your app's complexity, team size, and how predictable you need state changes to be.
🔹 High Level Architecture
Redux Toolkit — Unidirectional Data Flow
Components dispatch actions (plain objects describing "what happened") → Reducers (pure functions) compute new state → Store holds the single source of truth → Components read state via selectors and re-render when their selected slice changes.
Key Properties: Single source of truth. Every state change is traceable. Middleware can intercept any action. Time-travel debugging is built in.
Zustand — Direct Store Access
Components import a hook → The hook reads state and exposes mutator functions → Calling a mutator updates the store directly → Only components whose selected slice changed re-render.
Key Properties: No actions, no reducers, no dispatch. Components read and write directly. Store lives outside React's component tree. No <Provider> wrapper required.
Jotai — Atomic State Graph
State is defined as small independent atoms → Components subscribe to individual atoms → Derived atoms automatically compute values from other atoms → When a base atom changes, only its subscribers and dependent derived atoms re-evaluate.
Key Properties: Each atom is independent. Dependencies are tracked automatically. Re-renders happen at the atom level — the most granular of all three. Atoms compose bottom-up.
🔹 Redux Toolkit RTK Overview
What It Is
Redux Toolkit (RTK) is the official, opinionated way to use Redux. Created by the Redux team to eliminate the biggest complaint — too much boilerplate — RTK wraps Redux with sensible defaults, built-in Immer for immutable updates, and a standardized slice pattern.
Core Concepts
| Concept | What It Does |
|---|---|
| Store | Single object holding all app state |
| Slice | State + reducers + auto-generated actions for one feature |
| Action | Plain object describing "what happened" ({ type, payload }) |
| Reducer | Pure function: (currentState, action) → newState
|
| Selector | Function that extracts or derives data from the store |
| Thunk | Async action creator for side effects (API calls, etc.) |
| RTK Query | Built-in data fetching and caching layer |
| Middleware | Intercepts actions between dispatch and the reducer |
How Updates Flow
- Component calls
dispatch(action) - Middleware processes the action (logging, async, analytics)
- Reducer receives
(currentState, action)and returnsnewState - Store replaces its state reference
- All
useSelectorhooks re-evaluate - Only components whose selector output changed will re-render
Strengths
| Advantage | Details |
|---|---|
| Best debugging experience | Redux DevTools: time-travel, action log, state diff, action replay |
| Predictable state transitions | Every change goes through action → reducer — easy to trace and test |
| First-class async support |
createAsyncThunk + RTK Query handle loading, error, and caching out of the box |
| Strong conventions | Slices, selectors, thunks — clear patterns for any team member to follow |
| Massive ecosystem | Redux Saga, Redux Observable, redux-persist, thousands of integrations |
| Battle-tested at scale | Used by Instagram, Walmart, Twitch, Airbnb, and thousands of production apps |
| Excellent TypeScript support | Fully typed with minimal boilerplate |
Weaknesses
| Disadvantage | Details |
|---|---|
| More boilerplate than alternatives | Even with RTK, you still write slices, selectors, thunks, store setup |
| Steeper learning curve | Actions, reducers, selectors, middleware, thunks — many concepts to learn |
| Provider required | Must wrap the entire app in <Provider store={store}>
|
| Indirection | Dispatching in Component A → reducer processes → Component B re-renders |
| Larger bundle | ~13KB min+gzip (RTK + react-redux) — largest of the three |
🔹 Zustand Overview
What It Is
Zustand (German for "state") is a minimal, un-opinionated global state library built entirely on React hooks. Created by Poimandres (the team behind react-three-fiber and drei), it strips away every abstraction Redux adds and gives you the simplest possible API.
No reducers. No actions. No dispatch. No provider. No ceremony.
Core Concepts
| Concept | What It Does |
|---|---|
| create() | Creates a store and returns a custom hook |
| set() | Merges partial state into the store |
| get() | Reads the full current state (even outside React) |
| Selectors | Pick specific slices to avoid unnecessary re-renders |
| subscribe() | Listen to state changes outside React components |
| Middleware | Composable wrappers (persist, devtools, immer) |
How Updates Flow
- Component calls a mutator function directly (e.g.,
addItem(product)) - Mutator calls
set()which merges new state into the store - Zustand compares each subscriber's selected slice (via
Object.is) - Only components whose selected state changed will re-render
Strengths
| Advantage | Details |
|---|---|
| Extremely simple API | Entire API surface: create, set, get, selectors — fits in a tweet |
| Minimal boilerplate | No slices, no action types, no dispatch — just objects and functions |
| No Provider required | Store lives outside React tree — no wrapper component needed |
| Tiny bundle size | ~1.1KB min+gzip — roughly 10x smaller than RTK |
| Works outside React |
getState() and subscribe() work in vanilla JS, Node, or tests |
| Flexible middleware |
persist, devtools, immer, subscribeWithSelector — all composable |
| Fastest to prototype with | Go from zero to working global state in under 5 minutes |
Weaknesses
| Disadvantage | Details |
|---|---|
| No enforced structure | Easy to create messy "god stores" with 50+ properties and no boundaries |
| Weaker debugging | DevTools support exists via middleware, but not as rich as Redux time-travel |
| No built-in async conventions | You write async logic manually — no equivalent to createAsyncThunk or RTK Query |
| Manual selector memoization | No built-in createSelector — you need useShallow or manual equality checks |
| Hard to enforce at scale | Without team conventions, 10 developers will write 10 different patterns |
🔹 Jotai Overview
What It Is
Jotai (Japanese for "state") is built on the atomic state model inspired by Recoil. Instead of one centralized store, state is broken into the smallest possible independent pieces (atoms), which compose together to create derived state.
Created by Daishi Kato (who also contributes to Zustand and valtio), Jotai focuses on fine-grained reactivity — components only re-render when the specific atom they subscribe to changes.
Core Concepts
| Concept | What It Does |
|---|---|
| Primitive Atom | Holds a single value (read + write) |
| Derived Atom (read-only) | Computes a value from other atoms automatically |
| Derived Atom (read-write) | Computes a value + has custom write logic |
| Async Atom | Returns a Promise — integrates with React Suspense |
| useAtom() | Hook to read + write an atom |
| useAtomValue() | Hook to read only (no setter — no unnecessary re-renders) |
| useSetAtom() | Hook to write only (no value subscription) |
| Provider | Optional — scopes atoms to a subtree (useful for testing/SSR) |
| Atom Family | Creates one atom per dynamic key (e.g., one per list item) |
How Updates Flow
- Component calls
setAtom(newValue)on a primitive or write atom - The atom's value updates in Jotai's internal store
- All derived atoms that depend on it automatically re-compute
- Only components subscribed to changed atoms re-render
Jotai's dependency graph is automatic — when a derived atom reads another atom, Jotai tracks that dependency. When the source changes, the derived atom recalculates.
Strengths
| Advantage | Details |
|---|---|
| Most granular re-renders | Components only re-render when the exact atom they read changes |
| Derived state is first-class | Computed atoms are clean, automatic, and composable |
| Tiny bundle size | ~2.4KB min+gzip |
| React Suspense integration | Async atoms work natively with Suspense boundaries |
| No boilerplate | Define an atom in one line, use it in one line |
| Bottom-up architecture | Start with atoms, compose as needed — scales naturally with complexity |
| Atom families | Dynamic atoms for lists/maps — avoids re-renders of unrelated items |
Weaknesses
| Disadvantage | Details |
|---|---|
| Harder mental model | "Atoms" and "derived atoms" require a paradigm shift from centralized stores |
| No central overview of state | State is scattered across atom files — hard to see "the big picture" |
| Weakest debugging | No built-in time-travel; Jotai DevTools exist but are less mature |
| Action tracing is difficult | No action log — harder to answer "what changed this atom and why?" |
| Not ideal for very large teams | Without strict conventions, atom organization can become chaotic |
| Smaller ecosystem | Fewer middleware options and fewer Stack Overflow answers than Redux |
🔹 Side by Side Feature Comparison
| Feature | Redux Toolkit | Zustand | Jotai |
|---|---|---|---|
| Bundle Size | ~13KB (RTK + react-redux) | ~1.1KB | ~2.4KB |
| Boilerplate | Medium | Very Low | Low |
| Mental Model | Flux (actions + reducers) | Hook-based mutable store | Atomic state graph |
| Provider Required | ✅ Yes | ❌ No | ⚠️ Optional |
| Debugging (DevTools) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| Time-Travel Debugging | ✅ Built-in | ❌ No | ❌ No |
| Async Support | Excellent (Thunks + RTK Query) | Manual (inline async) | Good (async atoms + Suspense) |
| Data Fetching / Caching | RTK Query (built-in) | External (TanStack Query) | External (TanStack Query) |
| Middleware | Extensive ecosystem | persist, devtools, immer | Atom utilities (storage, family) |
| Derived / Computed State |
createSelector (Reselect) |
Manual (via get()) | First-class (derived atoms) |
| Re-render Granularity | Selector-based | Subscription-based | Atom-level (most granular) |
| TypeScript Support | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| SSR / Next.js | ✅ Most mature | ✅ Works (manual setup) | ✅ Works (via Provider) |
| React Native | ✅ Full support | ✅ Full support | ✅ Full support |
| Learning Curve | Medium–High | Low | Medium |
| Scalability | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| Community & Ecosystem | Massive | Growing fast | Growing |
| Best For | Enterprise apps | Small–mid apps | Complex UI / fine-grained |
🔹 Performance and Re render Behavior
Understanding re-render behavior is critical — unnecessary re-renders are the #1 performance problem in React apps.
How Each Library Triggers Re-renders
| Library | Strategy | What Causes a Re-render | What Does NOT Cause a Re-render |
|---|---|---|---|
| Redux Toolkit | Selector-based (reference equality) | Selector returns a new reference (!== previous) |
Unrelated state changes (selector returns same ref) |
| Zustand | Subscription + shallow compare | Selected state slice changes | Unrelated state changes (different selector) |
| Jotai | Atom-level subscription | The specific atom the component reads changes | Any atom the component did NOT subscribe to |
Re-render Scenario
Consider an app with users, cart, and ui state. A user adds an item to the cart:
| Component | Redux Toolkit | Zustand | Jotai |
|---|---|---|---|
| CartBadge | ✅ Re-renders (selector output changed) | ✅ Re-renders (selected slice changed) | ✅ Re-renders (cart atom changed) |
| UserList | ❌ No re-render (different selector) | ❌ No re-render (different slice) | ❌ No re-render (different atom) |
| Sidebar | ❌ No re-render (different selector) | ❌ No re-render (different slice) | ❌ No re-render (different atom) |
| Single product in a list of 100 | ❌ Entire list re-renders | ❌ Entire list re-renders | ✅ Only THAT product re-renders (via atom family) |
Performance Summary
| Aspect | Redux Toolkit | Zustand | Jotai |
|---|---|---|---|
| Default granularity | Good | Good | Best |
| List item updates | Re-renders list | Re-renders list | Re-renders one item (atom family) |
| Derived state performance | Good (Reselect) | Manual memoization | Automatic (derived atoms) |
| Initial setup overhead | Higher | Minimal | Minimal |
| Memory footprint | Medium | Small | Small |
💡 Rule of Thumb: For most apps, all three perform well enough. Jotai shines when you have large lists with frequent individual item updates — think design tools, spreadsheets, or real-time dashboards.
🔹 Middleware and DevTools
Redux Toolkit — Strongest Ecosystem
| Middleware | Purpose | Built-in? |
|---|---|---|
| Redux DevTools | Time-travel, action log, state diff, replay | ✅ Yes |
| redux-thunk | Async actions | ✅ Yes |
| RTK Query | Data fetching, caching, polling, invalidation | ✅ Yes |
| redux-persist | Persist state to localStorage / AsyncStorage | Plugin |
| redux-saga | Complex async flows (channels, forks, races) | Plugin |
| redux-observable | RxJS-based side effects | Plugin |
| Custom middleware | Logging, analytics, error reporting | Easy |
Zustand — Composable Middleware
| Middleware | Purpose | Built-in? |
|---|---|---|
devtools |
Connect to Redux DevTools | ✅ Yes |
persist |
Persist to localStorage / AsyncStorage | ✅ Yes |
immer |
Mutable-looking immutable updates | ✅ Yes |
subscribeWithSelector |
Fine-grained external subscriptions | ✅ Yes |
| Custom middleware | Wrap store creator with any logic | Easy |
Jotai — Atom Utilities
| Utility | Purpose |
|---|---|
atomWithStorage |
Persist atom to localStorage |
atomWithObservable |
Integrate with RxJS observables |
atomFamily |
Dynamic atom creation (one per key) |
selectAtom |
Derive with custom equality function |
atomWithReducer |
Redux-style reducer for a single atom |
loadable |
Handle async atom without Suspense |
| Jotai DevTools | Inspect atom values in browser |
DevTools Comparison
| Capability | Redux Toolkit | Zustand | Jotai |
|---|---|---|---|
| Action logging | ✅ Full | ⚠️ Via middleware | ❌ No actions |
| State inspection | ✅ Full | ✅ Via middleware | ⚠️ Basic |
| Time-travel | ✅ Built-in | ❌ No | ❌ No |
| State diff | ✅ Built-in | ⚠️ Limited | ❌ No |
| Action replay | ✅ Built-in | ❌ No | ❌ No |
⚠️ If your app deals with financial data, audit trails, or regulatory compliance, Redux Toolkit's debugging capabilities are a significant advantage.
🔹 TypeScript Support
| Aspect | Redux Toolkit | Zustand | Jotai |
|---|---|---|---|
| Auto-inference | Good (from initialState) | Needs explicit store interface | Great (from atom initial value) |
| Typed hooks | Requires defining typed wrappers | Type comes free with create<T>()
|
Hooks are generic automatically |
| IDE autocomplete | Excellent | Excellent | Excellent |
| Selector type safety | Full (via RootState type) | Full (via store type) | Full (via atom type) |
| Refactoring confidence | High | High | High |
| Overall TS experience | ⭐⭐⭐⭐⭐ (best documented) | ⭐⭐⭐⭐ (clean, less setup) | ⭐⭐⭐⭐ (most automatic) |
🔹 SSR and Next.js Compatibility
Server-Side Rendering introduces unique challenges: state must be initialized on the server, serialized, and hydrated on the client without leaking between requests.
| Aspect | Redux Toolkit | Zustand | Jotai |
|---|---|---|---|
| SSR maturity | Most mature | Requires manual patterns | Good (with Provider) |
| Per-request isolation | ✅ via next-redux-wrapper | ⚠️ Manual (React Context) | ✅ via Provider |
| Hydration support | Built-in with wrapper | Manual rehydration needed |
useHydrateAtoms utility |
| Next.js App Router | ✅ Well supported | ✅ Works with patterns | ✅ Works with Provider |
| Streaming SSR | ✅ Works | ✅ Works | ✅ Works with Suspense |
| Risk of state leaks | Low (well-documented) | Medium (store is singleton) | Low (Provider scopes state) |
💡 If SSR correctness is critical (e.g., e-commerce with personalized data), Redux Toolkit has the most battle-tested patterns. Zustand requires the most manual care since its stores are singletons by default.
🔹 Scalability and Team Collaboration
Redux Toolkit — Enterprise Grade
| Aspect | Assessment |
|---|---|
| Team size | Built for 10–100+ developers working in parallel |
| Feature isolation | Each feature has its own slice, selectors, thunks — clear boundaries |
| Code review | Actions and reducers are explicit — "what changed and why?" is clear |
| Onboarding | Clear conventions → new devs productive in days |
| Merge conflicts | Minimal — features don't touch each other's slices |
| Testing | Reducers are pure functions — easiest to unit test of all three |
| Code splitting | Reducers can be lazy-loaded per route |
Zustand — Small Team Sweet Spot
| Aspect | Assessment |
|---|---|
| Team size | Best for 1–5 developers |
| Feature isolation | Requires team discipline; "slice pattern" helps but isn't enforced |
| Code review | Harder — mutations happen inline, no formal action log |
| Onboarding | Instant — API is trivial to learn |
| Merge conflicts | Can happen if everyone edits the same store file |
| Testing | Test by calling store actions and asserting state |
| Code splitting | Not built-in, but stores are naturally separate files |
Jotai — UI Complexity Champion
| Aspect | Assessment |
|---|---|
| Team size | Best for 2–10 developers |
| Feature isolation | Atoms can be organized by feature folder |
| Code review | Harder — need to mentally trace atom dependency graph |
| Onboarding | Medium — atomic mental model requires a paradigm shift |
| Merge conflicts | Rare — atoms are small, independent files |
| Testing | Test by rendering components with specific atom values |
| Code splitting | Atoms are naturally code-split (only loaded when imported) |
🔹 How to Choose Decision Frameworks
Decision Flowchart
Start
│
├─ Is the app an enterprise product with 50+ screens? ─── YES ──→ Redux Toolkit
│
├─ Do multiple teams (5+) work on the same codebase? ─── YES ──→ Redux Toolkit
│
├─ Do you need time-travel debugging or action tracing? ─── YES ──→ Redux Toolkit
│
├─ Do you need built-in data fetching + caching (RTK Query)? ─── YES ──→ Redux Toolkit
│
├─ Is the app small-to-medium (< 30 screens)? ─── YES ──→ Zustand
│
├─ Do you want the simplest possible setup? ─── YES ──→ Zustand
│
├─ Do you need to access state outside React (vanilla JS)? ─── YES ──→ Zustand
│
├─ Is UI logic very complex with many derived values? ─── YES ──→ Jotai
│
├─ Do you need fine-grained re-renders for large lists? ─── YES ──→ Jotai
│
├─ Do you want native React Suspense integration? ─── YES ──→ Jotai
│
└─ Not sure? ──→ Start with Zustand, migrate to RTK if complexity grows
Decision Matrix
| Factor | Redux Toolkit | Zustand | Jotai |
|---|---|---|---|
| App has 100+ screens | ✅ Best choice | ⚠️ Can work | ⚠️ Can work |
| Multiple teams in parallel | ✅ Best choice | ❌ Gets messy | ⚠️ Needs conventions |
| Long-term maintainability | ✅ Best choice | ⚠️ Needs discipline | ⚠️ Needs discipline |
| Speed of development / prototyping | ⚠️ Slower setup | ✅ Best choice | ✅ Fast |
| Prototyping / MVP | ❌ Overkill | ✅ Best choice | ✅ Good |
| Complex derived state | ⚠️ Reselect works | ⚠️ Manual | ✅ Best choice |
| Fine-grained reactivity | ⚠️ With selectors | ⚠️ With selectors | ✅ Best choice |
| Design tools / spreadsheet-like UIs | ❌ Too coarse | ⚠️ Possible | ✅ Best choice |
| Debugging-critical (finance, health) | ✅ Best choice | ⚠️ Limited | ❌ Weakest |
| Smallest bundle size matters | ❌ Largest (~13KB) | ✅ Best (~1.1KB) | ✅ Small (~2.4KB) |
| Server-side rendering | ✅ Most mature | ⚠️ Manual setup | ✅ Good |
| Using state outside React | ⚠️ Possible | ✅ Best choice | ❌ React-only |
The "Classify First" Approach
Before choosing a library, classify what kind of state you actually have:
| State Type | Best Tool | NOT This |
|---|---|---|
| Server/API data | TanStack Query or RTK Query | Don't manually fetch in stores |
| Client UI state | RTK / Zustand / Jotai | Don't use for API caching |
| URL state | React Router / Next.js router | Don't duplicate URL params in store |
| Form state | React Hook Form / Formik | Don't manage forms in global state |
| Derived state | Selectors (RTK) / Derived atoms (Jotai) | Don't store computed values |
💡 Most apps need less global state than you think. If you classify first, you'll often find that 70% of your "state management problem" is actually a data fetching problem best solved by TanStack Query or RTK Query — not a store.
🔹 Real World Use Case Mapping
| Scenario | Best Choice | Why |
|---|---|---|
| Enterprise dashboard (50+ screens) | Redux Toolkit | Predictable state, DevTools, feature isolation, RTK Query for APIs |
| E-commerce platform | Redux Toolkit | Cart, auth, products, orders — many domains needing async data and team collaboration |
| Chat application | Redux Toolkit | Message history, user presence, typing indicators — many async streams to coordinate |
| Multi-brand / white-label product | Redux Toolkit | Consistent architecture across brands, shared reducers, brand-specific slices |
| Admin panel / internal tool | Zustand | Simple CRUD, small team, fast development, minimal ceremony |
| Side project / personal app | Zustand | Fastest to set up, smallest bundle, no overthinking |
| Mobile app (React Native) | Zustand | Smallest bundle, simplest setup, great with React Navigation |
| Marketing site with dynamic UI | Zustand or Jotai | Lightweight, minimal state, fast page loads |
| Design tool (like Figma/Canva) | Jotai | Hundreds of independent objects on canvas, fine-grained updates, derived properties |
| Spreadsheet / data grid app | Jotai | Cell-level reactivity via atom families, derived formulas via derived atoms |
| Complex form with many dependencies | Jotai | Each field as an atom, validation as derived atoms, no unnecessary re-renders |
| Real-time collaborative editor | Jotai + Zustand | Jotai for document atoms, Zustand for connection/presence state |
🔹 Migration Guidance
When to Migrate
| From → To | Trigger Signal |
|---|---|
| Zustand → Redux Toolkit | App has grown, team has grown, debugging is painful, need stricter conventions |
| Zustand → Jotai | Too many derived values computed manually, need fine-grained list updates |
| Redux Toolkit → Zustand | App is simpler than expected, Redux overhead isn't paying off, team is very small |
| Jotai → Redux Toolkit | Team can't trace state changes, need action logging, multiple teams joining |
Migration Best Practices
- Migrate one feature at a time — libraries can coexist during transition
- Never do a big-bang rewrite — incremental migration reduces risk
- Start with the most isolated feature — pick a feature with few cross-dependencies
- Run both libraries in parallel — old features use old lib, new features use new lib
- Delete old code only after new code is tested — don't leave dead state around
💡 Key Insight: All three libraries can coexist in the same app. A common production pattern is RTK Query or TanStack Query for server state, plus Zustand or Jotai for client/UI state.
🔹 Best Practices
General State Management
| Practice | Why |
|---|---|
| Classify your state first | Server → TanStack Query. Client → RTK/Zustand/Jotai. URL → router. Form → form library |
| Don't put everything in global state | Only shared, cross-component state belongs in a store |
| Derive, don't store | Computed values belong in selectors / derived atoms, not in state |
| Normalize entity data | Store lists as { ids: [], entities: {} } for O(1) lookups |
| Co-locate state with features | Keep state logic near the components that use it |
| Separate server state from client state | Use a data-fetching library for API data; use stores for UI state only |
Redux Toolkit Specific
| Practice | Why |
|---|---|
| One slice per feature domain | Clear ownership, easy to code-split |
| Use memoized selectors for all derived data | Prevents unnecessary re-renders |
| Use RTK Query instead of manual thunks for API calls | Built-in caching, polling, invalidation |
| Define typed hooks once, use everywhere | Full TypeScript safety with zero per-component boilerplate |
| Keep reducers under 200 lines | Split large slices into sub-slices when they grow |
Zustand Specific
| Practice | Why |
|---|---|
Always use selectors (useStore(s => s.field)) |
Prevents re-renders when unrelated state changes |
| Split into domain-specific stores | Avoid "god stores" — useCartStore, useUserStore, useUIStore |
Always enable devtools middleware |
Free debugging with no downside |
Use persist for state that survives refresh |
Cart, user preferences, auth tokens |
Use immer middleware for complex nested updates |
Cleaner syntax, fewer mutation bugs |
Jotai Specific
| Practice | Why |
|---|---|
| Keep atoms small and focused (one value each) | Don't create "object atoms" with 20 fields |
Use useAtomValue for read-only, useSetAtom for write-only |
Prevents unnecessary subscriptions and re-renders |
| Group related atoms in feature folders |
atoms/cart/, atoms/auth/, atoms/ui/
|
| Use atom families for dynamic / list data | Avoids re-renders of unrelated list items |
| Document atom dependency graphs for your team | Helps everyone understand what depends on what |
🔹 State Management Checklist
Before Choosing a Library
✅ Classified state types (server vs client vs URL vs form)
✅ Identified which state actually needs to be global
✅ Assessed team size and experience level
✅ Evaluated app complexity (screen count, async flows, derived data)
✅ Considered long-term maintainability needs
✅ Checked SSR / Next.js requirements
✅ Reviewed debugging requirements (time-travel, action logging, audit trails)
✅ Evaluated bundle size constraints (mobile, low-bandwidth users)
After Implementing
✅ Components subscribe to minimal state slices (not entire store)
✅ All derived data is computed (selectors / derived atoms), not stored
✅ Async operations handle loading, success, and error states
✅ Entity data is normalized where appropriate
✅ DevTools middleware is enabled in development
✅ Persisted state uses proper middleware (persist / atomWithStorage)
✅ Server state uses a data-fetching library, not manual fetches in stores
✅ TypeScript types are defined for all state shapes
✅ Feature state is co-located with feature components
🔹 Key Interview Takeaways
"Compare Redux Toolkit, Zustand, and Jotai"
Redux Toolkit follows the Flux pattern — single centralized store, actions describe events, reducers compute new state. Best for large-scale enterprise apps where predictability, debugging, and team conventions matter most.
Zustand is a minimal hook-based external store — no actions, no reducers, no provider. Best for small-to-medium apps where speed and simplicity are the priority.
Jotai uses the atomic model — independent atoms that compose into derived state. Best for complex UIs needing fine-grained reactivity, like design tools or spreadsheets.
"When would you migrate from one to another?"
Start with Zustand for speed. If the app grows, teams multiply, or debugging becomes painful — migrate to Redux Toolkit for structure. If you discover performance bottlenecks due to coarse re-renders — reach for Jotai.
"What about performance?"
All three are fast enough for most apps. The difference shows up at scale: Redux uses selector-based re-renders (good), Zustand uses subscription-based (good), Jotai uses atom-level subscriptions (best granularity). For list-heavy UIs, Jotai's atom families avoid re-rendering the entire list when one item changes.
"Can they coexist?"
Yes. A common production pattern is RTK Query or TanStack Query for server state, plus Zustand or Jotai for client/UI state. They manage different concerns and don't conflict.
"If you had to pick one default?"
Zustand for most new projects — simplest API, smallest bundle, fastest time-to-value. Redux Toolkit if the project will be large, long-lived, or worked on by many teams.
🔹 Further Reading and Resources
| Resource | Type |
|---|---|
| Redux Toolkit Official Docs | Documentation |
| Zustand GitHub | Documentation |
| Jotai Official Docs | Documentation |
| RTK Query Overview | Guide |
| Zustand — Slice Pattern | Guide |
| Jotai — Atoms in Practice | Guide |
| Mark Erikson — Why Redux Toolkit | Blog |
| Daishi Kato — Zustand, Jotai, Valtio | Blog |
| TanStack Query — for server state | Library |
More Details:
Get all articles related to system design
Hastag: SystemDesignWithZeeshanAli
Git: https://github.com/ZeeshanAli-0704/front-end-system-design
Top comments (0)