What are the alternatives to Redux?

👨‍💻 Frontend Developer 🟡 Often Asked 🎚️ Medium
#React #State Management

Brief Answer

Main alternatives to Redux for state management:

  1. React Context + useReducer — built-in React solution 📦
  2. MobX — reactive state management 🔄
  3. Zustand — minimalist hook-based solution 🪝
  4. Recoil — atomic state management from Facebook ⚛️
  5. Jotai — primitive atomic approach 🧩
  6. XState — finite state machine based management 🤖
  7. Valtio — proxy objects for state 🔍
  8. React Query — for server state 🌐
// Example with Context API + useReducer
const initialState = { count: 0 };
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    default: return state;
  }
};
 
// Usage
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'increment' });

Full Answer

Redux has long been the standard for state management in React applications, but today there are many alternatives that may be simpler, lighter, or better suited for specific tasks. 🔄

1. React Context API + useReducer

A built-in React solution suitable for small to medium applications:

// Create context
const CounterContext = createContext();
 
// Provider with useReducer
function CounterProvider({ children }) {
  const [state, dispatch] = useReducer(
    (state, action) => {
      switch (action.type) {
        case 'increment': return { count: state.count + 1 };
        case 'decrement': return { count: state.count - 1 };
        default: return state;
      }
    },
    { count: 0 }
  );
 
  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      {children}
    </CounterContext.Provider>
  );
}
 
// Usage in component
function Counter() {
  const { state, dispatch } = useContext(CounterContext);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </>
  );
}

Advantages:

  • Built into React
  • No external dependencies
  • Familiar API (similar to Redux)

Disadvantages:

  • Can lead to deeply nested providers
  • No developer tools by default
  • Less efficient for complex states

2. MobX

A library using observable objects and reactions:

// Define state
import { makeObservable, observable, action } from 'mobx';
import { observer } from 'mobx-react-lite';
 
class CounterStore {
  count = 0;
  
  constructor() {
    makeObservable(this, {
      count: observable,
      increment: action,
      decrement: action
    });
  }
  
  increment = () => {
    this.count++;
  }
  
  decrement = () => {
    this.count--;
  }
}
 
const counterStore = new CounterStore();
 
// Component with observation
const Counter = observer(() => {
  return (
    <>
      Count: {counterStore.count}
      <button onClick={counterStore.increment}>+</button>
      <button onClick={counterStore.decrement}>-</button>
    </>
  );
});

Advantages:

  • Automatic dependency tracking
  • Less boilerplate code
  • High performance

Disadvantages:

  • Uses classes and decorators (OOP approach)
  • Can be harder to understand reactivity
  • Less explicit data flow

3. Zustand

A minimalist library with a simple hook-based API:

import create from 'zustand';
 
// Create store
const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 }))
}));
 
// Usage in component
function Counter() {
  const { count, increment, decrement } = useCounterStore();
  
  return (
    <>
      Count: {count}
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </>
  );
}

Advantages:

  • Minimalist API
  • No providers required
  • Supports Redux dev tools
  • Easy to combine states

Disadvantages:

  • Less structured approach for large applications
  • No built-in middleware support

4. Recoil

An experimental library from Facebook for atomic state management:

import { atom, useRecoilState } from 'recoil';
 
// Define atom
const countAtom = atom({
  key: 'countState',
  default: 0
});
 
// Usage in component
function Counter() {
  const [count, setCount] = useRecoilState(countAtom);
  
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
    </>
  );
}
 
// Wrap application in RecoilRoot
function App() {
  return (
    <RecoilRoot>
      <Counter />
    </RecoilRoot>
  );
}

Advantages:

  • Atomic approach to state
  • Selectors for derived data
  • Good performance with partial updates

Disadvantages:

  • Experimental status
  • Requires root provider
  • More code for complex interactions

5. Jotai

A primitive atomic library inspired by Recoil:

import { atom, useAtom } from 'jotai';
 
// Create atom
const countAtom = atom(0);
 
// Derived atoms
const doubleCountAtom = atom(
  (get) => get(countAtom) * 2
);
 
// Usage in component
function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const [doubleCount] = useAtom(doubleCountAtom);
  
  return (
    <>
      Count: {count} (Double: {doubleCount})
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
    </>
  );
}

Advantages:

  • Lightweight (less than 2KB)
  • Simple API
  • Good atom composition

Disadvantages:

  • Less mature ecosystem
  • Less documentation and examples

6. XState

A library for state management based on finite state machines:

import { createMachine, assign } from 'xstate';
import { useMachine } from '@xstate/react';
 
// Define state machine
const counterMachine = createMachine({
  id: 'counter',
  initial: 'active',
  context: { count: 0 },
  states: {
    active: {
      on: {
        INCREMENT: {
          actions: assign({ count: (ctx) => ctx.count + 1 })
        },
        DECREMENT: {
          actions: assign({ count: (ctx) => ctx.count - 1 })
        }
      }
    }
  }
});
 
// Usage in component
function Counter() {
  const [state, send] = useMachine(counterMachine);
  
  return (
    <>
      Count: {state.context.count}
      <button onClick={() => send('INCREMENT')}>+</button>
      <button onClick={() => send('DECREMENT')}>-</button>
    </>
  );
}

Advantages:

  • Predictable state transitions
  • State visualization
  • Well-suited for complex business logic

Disadvantages:

  • Steeper learning curve
  • Overkill for simple cases
  • More boilerplate code

7. Valtio

A proxy-oriented library for state management:

import { proxy, useSnapshot } from 'valtio';
 
// Create proxy state
const state = proxy({ count: 0 });
 
// State mutations
const actions = {
  increment: () => { state.count++ },
  decrement: () => { state.count-- }
};
 
// Usage in component
function Counter() {
  const snap = useSnapshot(state);
  
  return (
    <>
      Count: {snap.count}
      <button onClick={actions.increment}>+</button>
      <button onClick={actions.decrement}>-</button>
    </>
  );
}

Advantages:

  • Intuitive state mutations
  • Simple API
  • Good performance

Disadvantages:

  • Less explicit data flow
  • Can lead to uncontrolled mutations

8. React Query

A specialized library for server state management:

import { useQuery, useMutation, QueryClient, QueryClientProvider } from 'react-query';
 
// Setup client
const queryClient = new QueryClient();
 
// Component with queries
function TodoList() {
  // Fetch data
  const { data: todos } = useQuery('todos', fetchTodos);
  
  // Mutate data
  const mutation = useMutation(addTodo, {
    onSuccess: () => {
      queryClient.invalidateQueries('todos');
    }
  });
  
  return (
    <>
      <ul>
        {todos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
      </ul>
      <button onClick={() => mutation.mutate({ title: 'New Todo' })}>
        Add Todo
      </button>
    </>
  );
}
 
// Wrap application
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <TodoList />
    </QueryClientProvider>
  );
}

Advantages:

  • Caching and invalidation
  • Automatic refetching
  • Loading and error handling
  • Optimistic updates

Disadvantages:

  • Specialized for server state
  • Doesn’t completely replace client state management

Comparison with Redux

LibrarySizeComplexityPerformanceEcosystem
ReduxMediumHighGoodExtensive
Context0KBLowMediumBuilt-in
MobXSmallMediumExcellentGood
ZustandSmallLowGoodGrowing
RecoilMediumMediumGoodGrowing
JotaiSmallLowGoodNew
XStateMediumHighGoodGrowing
ValtioSmallLowExcellentNew
React QueryMediumMediumExcellentGood

When to Choose Redux Alternatives

  1. Small applications — Context API or Zustand
  2. Medium applications — MobX, Jotai, or Valtio
  3. Complex applications with business logic — XState
  4. Applications with frequent UI updates — MobX or Valtio
  5. Applications focused on server data — React Query + lightweight UI solution

Best Practices

  1. Choose tools based on task size — don’t use heavy solutions for simple tasks 📏
  2. Separate client and server state — use specialized tools 🔄
  3. Start simple — add complexity only when necessary 🚀
  4. Consider team experience — familiar tools increase productivity 👥
  5. Test performance — some solutions may work slower on large applications 🔍

Conclusion

Modern Redux alternatives offer simpler and more specialized solutions for state management:

  • Context + useReducer — for simple cases
  • MobX/Valtio — for reactive approach
  • Zustand/Jotai/Recoil — for balance of simplicity and power
  • XState — for complex state logic
  • React Query — for server data

The choice depends on project size, performance requirements, and team preferences. 🎯