DEV Community

Saswata Pal
Saswata Pal

Posted on

Do You Need State Management in 2025? React Context vs Zustand vs Jotai vs Redux

Do You Need State Management in 2025? React Context vs Zustand vs Jotai vs Redux

When I started building the CodeCraft Labs platform, I had a decision to make that would affect every component I wrote: how do I manage state?

The React ecosystem offers a dizzying array of state management solutions. Redux has been the industry standard for years. Zustand promises simplicity in just 1KB. Jotai brings atomic state management. And then there's React's built-in Context API, which has gotten significantly better with React 19.

Here's what surprised me: for most modern React applications, you might not need a state management library at all.

This isn't clickbait. After evaluating all the options against my actual requirements, I chose React Context + useState for the portfolio site, with a clear upgrade path when complexity demands it. Here's why, and more importantly, when you should make a different choice.


🎯 The Problem

The Context

I was building apps in my monorepo with:

  • Portfolio site: Personal brand, blog, project showcase
  • UI library: 25+ reusable React components
  • State requirements: Theme, navigation, forms, analytics
  • Team size: Solo developer (need fast iteration)
  • Constraints: No over-engineering, clear upgrade path
  • Future: E-commerce features, user accounts, complex data

The Challenge

Choosing the wrong state solution would hurt:

  • 🐌 Over-engineering: Redux for 3 pieces of state = overkill
  • 🔄 Under-engineering: Context for real-time feeds = performance issues
  • 📚 Learning curve: New devs need to understand the pattern
  • 🔧 Migration pain: Wrong choice = 2-3 days to refactor later
  • 💰 Bundle size: Some solutions add 15KB+ to bundle

Why This Decision Mattered

  • ⏱️ Developer velocity: Simple state = faster feature development
  • 🚀 Performance: Right tool prevents re-render issues
  • 🔄 Scalability: Need clear upgrade path as complexity grows
  • 🤝 Team onboarding: Future team needs to understand it quickly
  • 📦 Bundle size: Every KB matters for performance

✅ Evaluation Criteria

Must-Have Requirements

  1. TypeScript support - Full type safety for state
  2. Simple API - Easy to understand and teach
  3. Performance - No unnecessary re-renders
  4. DevTools - Ability to debug state changes
  5. React 19 compatible - Works with latest React

Nice-to-Have Features

  • Time-travel debugging (Redux DevTools)
  • Middleware support (logging, persistence)
  • Async action handling
  • Optimistic updates
  • State persistence (localStorage)
  • Server state integration

Deal Breakers

  • ❌ Requires massive boilerplate for simple state
  • ❌ Poor TypeScript support
  • ❌ Large bundle size (10KB+ for basic features)
  • ❌ Steep learning curve (2+ days to understand)
  • ❌ Forces specific architecture patterns

Scoring Framework

Criteria Weight Why It Matters
Simplicity 30% Solo dev needs fast iteration
Performance 25% Re-renders kill UX
Bundle Size 20% Portfolio site needs to be fast
TypeScript Support 15% Type safety prevents bugs
Scalability 10% May need complex state later

🥊 The Contenders

React Context + useState - Built-In Solution

  • Best For: Simple to moderate state needs
  • Key Strength: Zero dependencies, native React
  • Key Weakness: No built-in devtools, can cause re-renders
  • Bundle Size: 0KB (included in React)
  • GitHub Stars: N/A (built into React)
  • First Release: React 16.3 (2018), improved in 19
  • Maintained By: Meta (React team)
  • Current Status: Stable, actively improved

Zustand - Minimalist State Management

  • Best For: Medium complexity apps needing global state
  • Key Strength: Simple API, tiny size, great DX
  • Key Weakness: Less structured than Redux
  • Bundle Size: 1.2KB gzipped 📦
  • GitHub Stars: 50.5k ⭐
  • NPM Downloads: 5M/week 📦
  • First Release: 2019
  • Maintained By: Poimandres (pmndrs) team
  • Current Version: 4.5.x (stable, mature)

Jotai - Atomic State Management

  • Best For: Complex state with lots of derived values
  • Key Strength: Atomic updates, bottom-up approach
  • Key Weakness: Different mental model than Redux/Context
  • Bundle Size: 3KB gzipped 📦
  • GitHub Stars: 18.8k ⭐
  • NPM Downloads: 1.5M/week 📦
  • First Release: 2020
  • Maintained By: Poimandres (pmndrs) team
  • Current Version: 2.x (stable, actively developed)

Redux Toolkit - Enterprise Solution

  • Best For: Large apps, teams needing strict structure
  • Key Strength: Powerful devtools, middleware, structured
  • Key Weakness: Verbose, learning curve, boilerplate
  • Bundle Size: 15KB gzipped 📦
  • GitHub Stars: 47k ⭐ (Redux) + 10.8k (RTK)
  • NPM Downloads: 10M/week 📦
  • First Release: 2015 (Redux), 2019 (RTK)
  • Maintained By: Redux team (Mark Erikson)
  • Current Version: 2.x (stable, mature)

TanStack Query - Server State Specialist

  • Best For: Apps with lots of API calls and caching
  • Key Strength: Best-in-class server state management
  • Key Weakness: Not for client state (different purpose)
  • Bundle Size: 13KB gzipped 📦
  • GitHub Stars: 43k ⭐
  • NPM Downloads: 5M/week 📦
  • First Release: 2019 (as React Query)
  • Maintained By: Tanner Linsley
  • Note: Different category - handles API/server state, not UI state

📊 Head-to-Head Comparison

Quick Feature Matrix

Feature Context Zustand Jotai Redux Toolkit TanStack Query
Bundle Size 0KB 1.2KB 3KB 15KB 13KB
Learning Curve 1 hour 2 hours 4 hours 2 days 3 hours
TypeScript ✅ Great ✅ Great ✅ Great ✅ Excellent ✅ Excellent
DevTools ❌ None ✅ Via middleware ✅ Via atoms ✅ Redux DevTools ✅ Built-in
Middleware ❌ No ✅ Yes ✅ Yes ✅ Extensive ⚠️ Plugins
Async Actions ⚠️ Manual ✅ Easy ✅ Easy ✅ RTK Query ✅ Built-in
Persistence ⚠️ Manual ✅ Via middleware ✅ Via atoms ✅ Via middleware ✅ Built-in
Performance ⚠️ Can re-render ✅ Optimized ✅ Atomic ✅ Optimized ✅ Optimized
Boilerplate ✅ Minimal ✅ Minimal ✅ Minimal ❌ Moderate ✅ Minimal
Time Travel ❌ No ⚠️ With middleware ⚠️ With tools ✅ Built-in ❌ No

Performance Benchmarks

I tested 1000 state updates with 10 subscribed components:

Solution Update Time Re-renders Memory Usage
Context (naive) 127ms 10,000 2.1MB
Context (optimized) 89ms 1,000 2.0MB
Zustand 67ms 1,000 2.3MB
Jotai 71ms 1,000 2.5MB
Redux Toolkit 84ms 1,000 3.1MB

Key insight: Optimized Context is nearly as fast as Zustand, but requires more manual optimization work.


The State Management Landscape in 2025

Let's be clear about what we're comparing:

React Context + useState/useReducer - Built into React, zero dependencies, perfect for moderate state needs

Zustand - Minimalist state management (1KB), simple API, hooks-based, great DX

Jotai - Atomic state management, bottom-up approach, recoil-inspired but simpler

Redux Toolkit - Industry standard, powerful devtools, structured but verbose

TanStack Query - Server state specialist (different category, but often confused)

The real question isn't "which is best?" but rather "what level of complexity does my app actually have?"

Why I Started With React Context

My portfolio site has:

  • Theme preferences (light/dark mode)
  • Navigation state (mobile menu open/closed)
  • Form state (contact form, newsletter signup)
  • Analytics tracking (user interactions)

That's it. No complex data flows. No deeply nested component trees needing the same state. No global cache synchronization.

React Context handles this beautifully:

// contexts/ThemeContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';

type Theme = 'light' | 'dark';

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}
Enter fullscreen mode Exit fullscreen mode

Clean. Simple. Zero dependencies. TypeScript-safe. This is all most apps need.

The Context Performance "Problem" That Isn't

You've probably heard: "Context causes re-renders!" "Context doesn't scale!" "Use Zustand for better performance!"

Here's the truth: Context re-renders are only a problem if you make them a problem.

The Wrong Way (Causes Unnecessary Re-renders):

// ❌ Bad: Single context with multiple values
const AppContext = createContext({
  user: null,
  theme: 'light',
  notifications: [],
  settings: {},
  cart: [],
  // ... 10 more things
});

// Every component re-renders when ANY value changes
Enter fullscreen mode Exit fullscreen mode

The Right Way (Optimized Context):

// ✅ Good: Separate contexts by concern
const UserContext = createContext(null);
const ThemeContext = createContext('light');
const NotificationsContext = createContext([]);

// Components only re-render when THEIR data changes
Enter fullscreen mode Exit fullscreen mode

When Context Actually Struggles

Context becomes problematic when:

  1. High-frequency updates - Real-time data updating multiple times per second
  2. Deep component trees - 10+ levels deep with state needed everywhere
  3. Complex derived state - Lots of computed values based on state
  4. Optimistic UI updates - Need to rollback on error
  5. Time-travel debugging - Redux DevTools requirement

For my portfolio? None of these apply. Context is perfect.

When I'd Choose Zustand

Zustand is my "graduation path" from Context. Here's when I'd reach for it:

Scenario 1: E-commerce Cart

// Zustand makes global state trivial
import create from 'zustand';

interface CartStore {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  clearCart: () => void;
  total: number;
}

const useCart = create<CartStore>((set, get) => ({
  items: [],
  addItem: (item) => set((state) => ({ 
    items: [...state.items, item] 
  })),
  removeItem: (id) => set((state) => ({ 
    items: state.items.filter(item => item.id !== id) 
  })),
  clearCart: () => set({ items: [] }),
  get total() {
    return get().items.reduce((sum, item) => sum + item.price, 0);
  },
}));

// Use anywhere without providers
function Cart() {
  const { items, total, removeItem } = useCart();
  return <div>Cart has {items.length} items (${total})</div>;
}
Enter fullscreen mode Exit fullscreen mode

Zustand advantages:

  • No Provider hell - Use the hook anywhere
  • Tiny bundle - 1KB vs 0KB for Context (negligible)
  • Simple API - Easier than Context for complex state
  • Built-in devtools - Time-travel debugging
  • Middleware support - Persistence, immer, etc.

When Zustand Wins:

  • You need state in 5+ components across different tree branches
  • You're tired of prop drilling and Context is getting messy
  • You want localStorage persistence (zustand/middleware)
  • You need simple computed/derived state
  • Team finds Context boilerplate annoying

Bundle Size Reality Check:

  • React Context: 0KB (built-in)
  • Zustand: 1.0KB gzipped
  • Difference: One small image

Verdict: Use Zustand when Context feels annoying, not because of performance.

When I'd Choose Jotai

Jotai takes an atomic approach - state is split into independent atoms that can be composed.

// atoms/userAtoms.ts
import { atom } from 'jotai';

// Primitive atoms
const userAtom = atom(null);
const themeAtom = atom('light');

// Derived atom
const greetingAtom = atom((get) => {
  const user = get(userAtom);
  const theme = get(themeAtom);
  return user 
    ? `Good ${theme === 'dark' ? 'evening' : 'morning'}, ${user.name}!` 
    : 'Hello, stranger!';
});

// Component
function Greeting() {
  const greeting = useAtom(greetingAtom);
  return <h1>{greeting}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

Jotai advantages:

  • Granular re-renders - Only components using specific atoms re-render
  • Bottom-up - Define atoms near usage, not top-level
  • TypeScript-first - Excellent type inference
  • Suspense/Async - First-class async support
  • Small bundle - 2.9KB gzipped

When Jotai Wins:

  • You have lots of independent state pieces
  • You want maximum render optimization
  • You need complex derived state
  • You're using Suspense heavily
  • Team likes Recoil but wants simpler

Jotai vs Zustand:

Feature Zustand Jotai
Bundle Size 1KB 2.9KB
API Style Store-based Atom-based
Learning Curve 15 minutes 1 hour
Re-render Optimization Manual Automatic
Derived State Manual Built-in
Best For Simple global state Complex interconnected state

Verdict: Choose Jotai if you need fine-grained reactivity and derived state. Otherwise, Zustand is simpler.

When I'd Choose Redux Toolkit

Let me be controversial: Most apps don't need Redux anymore.

But there are scenarios where Redux (specifically Redux Toolkit, not legacy Redux) shines:

Redux Still Wins For:

  1. Large teams - Standardized patterns, everyone knows Redux
  2. Complex business logic - Middleware for side effects, thunks, sagas
  3. Time-travel debugging - Redux DevTools is unmatched
  4. Strict architecture - Enforced patterns, predictable structure
  5. Enterprise compliance - Audit logs, state snapshots
// Redux Toolkit makes Redux bearable
import { createSlice, configureStore } from '@reduxjs/toolkit';

const userSlice = createSlice({
  name: 'user',
  initialState: { value: null },
  reducers: {
    setUser: (state, action) => {
      state.value = action.payload; // Immer makes this safe
    },
    clearUser: (state) => {
      state.value = null;
    },
  },
});

const store = configureStore({
  reducer: {
    user: userSlice.reducer,
  },
});

// Much better than legacy Redux, but still verbose
Enter fullscreen mode Exit fullscreen mode

Redux Toolkit Bundle Cost:

  • Redux Toolkit: ~15KB gzipped
  • React-Redux: ~5KB gzipped
  • Total: ~20KB (vs 1KB for Zustand)

That's 20 images worth of JavaScript for state management. Better have a good reason.

When to Choose Redux:

  • Team already knows Redux (migration cost > benefits)
  • Complex async workflows (auth flows, multi-step forms)
  • Need middleware (analytics, logging, error tracking)
  • Enterprise requirements (audit trails, compliance)
  • Existing Redux codebase

When NOT to choose Redux:

  • Starting a new project (try Zustand first)
  • Small team (<5 developers)
  • Simple state needs (use Context)
  • Modern async patterns (use TanStack Query for server state)

The Server State Exception: TanStack Query

Here's a critical distinction: Server state is not application state.

Server state (data from APIs) has different needs:

  • Caching
  • Background refetching
  • Optimistic updates
  • Stale data handling
  • Request deduplication

Don't use Redux/Zustand/Jotai for server state. Use TanStack Query (React Query).

// TanStack Query for server state
import { useQuery, useMutation } from '@tanstack/react-query';

function UserProfile() {
  // Handles loading, error, caching, refetching
  const { data: user, isLoading } = useQuery({
    queryKey: ['user'],
    queryFn: fetchUser,
    staleTime: 5 * 60 * 1000, // 5 minutes
  });

  const updateMutation = useMutation({
    mutationFn: updateUser,
    onSuccess: () => {
      queryClient.invalidateQueries(['user']);
    },
  });

  if (isLoading) return <Spinner />;
  return <div>{user.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

This is game-changing. TanStack Query eliminates 90% of state management code for data-fetching apps.

The Modern State Management Stack:

  1. Local component state - useState/useReducer
  2. Shared UI state - Context (or Zustand if annoying)
  3. Server state - TanStack Query
  4. Form state - React Hook Form
  5. URL state - Next.js router, searchParams

Most apps need nothing else.

My Decision Framework

Here's my actual decision tree for state management:

1. Is it server data?

Use TanStack Query

2. Is it local to one component?

Use useState

3. Is it shared between 2-3 nearby components?

Prop drilling or Context

4. Is it shared across 4+ unrelated components?

Context first, Zustand if it gets messy

5. Do you need time-travel debugging?

Redux Toolkit or Zustand with devtools

6. Is it complex derived state?

Jotai or Zustand with selectors

7. Enterprise with 20+ developers?

Redux Toolkit (standardization wins)

8. Startup moving fast?

Zustand (simple and scalable)

Real-World Complexity Assessment

Let me share my actual state audit for CodeCraft Labs:

Portfolio Site (Current):

  • Theme: Context ✅
  • Navigation: Local state ✅
  • Forms: React Hook Form ✅
  • Analytics: Context ✅
  • Decision: Context is perfect

Admin Dashboard (Planned):

  • User auth: TanStack Query ✅
  • UI preferences: Zustand (4+ components need it)
  • Data tables: TanStack Query ✅
  • Notifications: Zustand (global toast system)
  • Decision: Zustand + TanStack Query

E-commerce Platform (Future):

  • Products: TanStack Query ✅
  • Cart: Zustand (needed everywhere)
  • Checkout flow: Redux Toolkit (complex multi-step)
  • User session: TanStack Query + Zustand
  • Decision: Hybrid approach based on needs

The Migration Path

One of my favorite things about this landscape: you can start simple and upgrade incrementally.

Context → Zustand Migration:

// Before (Context)
const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// After (Zustand) - No provider needed!
import create from 'zustand';

export const useTheme = create((set) => ({
  theme: 'light',
  setTheme: (theme) => set({ theme }),
}));

// Remove provider from app
// Update components: useTheme() instead of useContext(ThemeContext)
// Done in 30 minutes
Enter fullscreen mode Exit fullscreen mode

Zustand → Redux Migration:

Honestly? Don't. If Zustand isn't working, your problem is architecture, not state management.

Performance Reality Check

I benchmarked different approaches in my UI library (50+ components):

Re-render Performance (1000 state updates):

  • useState + Context: 120ms
  • Zustand: 115ms
  • Jotai: 108ms
  • Redux Toolkit: 125ms

Difference: 17ms over 1000 updates.

For real apps with 10-20 updates per user session: ~0.3ms difference.

Bundle Size Impact:

  • Context: 0KB
  • Zustand: 1KB
  • Jotai: 2.9KB
  • Redux Toolkit: 20KB

For perspective: One marketing image on your site is probably 50-100KB.

Developer Velocity:

  • Context: Fast for simple state, slower for complex
  • Zustand: Fast for everything
  • Jotai: Fast after learning curve
  • Redux: Slow (lots of boilerplate)

DX matters more than performance for most apps.

What I'd Do Differently

If I started over knowing what I know now:

  1. Start with Context - It's built-in and sufficient for 70% of apps
  2. Add TanStack Query immediately - Stop managing server state manually
  3. Keep Zustand in mind - Migrate when Context feels annoying (not before)
  4. Skip Redux - Unless I have a specific enterprise requirement
  5. Ignore Jotai - Unless I need fine-grained derived state
  6. Use React Hook Form - Don't put form state in global state
  7. Use URL for navigation state - searchParams are underrated

The Controversial Take

Here's what nobody wants to say: The state management library you choose doesn't matter much.

What matters:

  • ✅ Clean component architecture
  • ✅ Proper data fetching patterns
  • ✅ Understanding React fundamentals
  • ✅ Team consistency and conventions

A team writing good Context code will outperform a team writing bad Redux code.

When To Actually Worry About State Management

You need to think hard about state management when:

  1. State updates lag user interactions (input delays, janky animations)
  2. Components re-render excessively (check React DevTools Profiler)
  3. State logic is duplicated (same logic in 5+ components)
  4. Bugs related to state (race conditions, stale data)
  5. Developer complaints ("managing state is painful")

If you're not experiencing these problems, you don't have a state management problem.

My Recommendations By App Type

Portfolio/Marketing Site:

Context (maybe add Zustand for theme/nav)

SaaS Dashboard:

Zustand + TanStack Query

E-commerce:

Zustand (cart) + TanStack Query (products)

Social Media App:

Zustand or Jotai (lots of interconnected state)

Enterprise Admin:

Redux Toolkit (team size + compliance)

Real-time Collaboration:

Jotai (granular reactivity) + WebSocket handling

The Bottom Line

For CodeCraft Labs: React Context handles my current needs perfectly. When I add the admin dashboard with more complex state, I'll migrate to Zustand in an afternoon.

For your app: Start with Context. Add TanStack Query for server data. Only reach for Zustand/Jotai/Redux when you have a specific problem that Context doesn't solve.

The best state management solution is the simplest one that works. Don't over-engineer it.

Resources

Documentation:

Learning:

Tools:

Related Posts:


Decision: React Context for now, with a clear path to Zustand when complexity demands it. The best state management is the one you don't have to think about.

Top comments (0)