אנליטיק לוגו
אנליטיק
חזרה

Modern State Management

Comparing different state management solutions for modern apps.

Modern State Management

State management is one of the most critical aspects of building scalable React applications. This guide explores various state management solutions and helps you choose the right approach for your project.

Understanding State in React

State in React applications can be categorized into several types:

  1. Local Component State: Data that belongs to a single component
  2. Shared State: Data that multiple components need to access
  3. Global State: Application-wide data like user authentication
  4. Server State: Data that comes from external APIs
  5. URL State: Data stored in the URL (query params, route params)

Built-in React Solutions

useState and useReducer

Perfect for local and moderate shared state:

// Simple local state
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

// Complex state with useReducer
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.text, done: false }];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, done: !todo.done } : todo
      );
    case 'DELETE_TODO':
      return state.filter(todo => todo.id !== action.id);
    default:
      return state;
  }
}

function TodoApp() {
  const [todos, dispatch] = useReducer(todoReducer, []);
  
  const addTodo = (text) => {
    dispatch({ type: 'ADD_TODO', text });
  };
  
  return (
    <div>
      <TodoForm onSubmit={addTodo} />
      <TodoList todos={todos} dispatch={dispatch} />
    </div>
  );
}

Context API

Great for avoiding prop drilling:

const ThemeContext = createContext();
const UserContext = createContext();

function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState(null);

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <Header />
        <MainContent />
        <Footer />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// Custom hooks for cleaner usage
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

function useUser() {
  const context = useContext(UserContext);
  if (!context) {
    throw new Error('useUser must be used within UserProvider');
  }
  return context;
}

Third-Party Solutions

Zustand

Lightweight and simple state management:

import { create } from 'zustand';

const useStore = create((set, get) => ({
  // State
  count: 0,
  user: null,
  todos: [],
  
  // Actions
  increment: () => set(state => ({ count: state.count + 1 })),
  decrement: () => set(state => ({ count: state.count - 1 })),
  
  setUser: (user) => set({ user }),
  
  addTodo: (text) => set(state => ({
    todos: [...state.todos, { id: Date.now(), text, done: false }]
  })),
  
  toggleTodo: (id) => set(state => ({
    todos: state.todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    )
  })),
  
  // Computed values
  get completedTodos() {
    return get().todos.filter(todo => todo.done);
  }
}));

// Usage in components
function Counter() {
  const { count, increment, decrement } = useStore();
  
  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  );
}

Redux Toolkit

Modern Redux with less boilerplate:

import { createSlice, configureStore } from '@reduxjs/toolkit';

// Slice definition
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => {
      state.value += 1; // Immer makes this immutable
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    }
  }
});

const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo: (state, action) => {
      state.push({
        id: Date.now(),
        text: action.payload,
        done: false
      });
    },
    toggleTodo: (state, action) => {
      const todo = state.find(todo => todo.id === action.payload);
      if (todo) {
        todo.done = !todo.done;
      }
    }
  }
});

// Store configuration
const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
    todos: todosSlice.reducer
  }
});

// Usage with React-Redux
import { useSelector, useDispatch } from 'react-redux';

function Counter() {
  const count = useSelector(state => state.counter.value);
  const dispatch = useDispatch();
  
  return (
    <div>
      <button onClick={() => dispatch(counterSlice.actions.decrement())}>-</button>
      <span>{count}</span>
      <button onClick={() => dispatch(counterSlice.actions.increment())}>+</button>
    </div>
  );
}

Jotai

Atomic approach to state management:

import { atom, useAtom } from 'jotai';

// Atomic state
const countAtom = atom(0);
const userAtom = atom(null);
const todosAtom = atom([]);

// Derived atoms
const completedTodosAtom = atom(
  (get) => get(todosAtom).filter(todo => todo.done)
);

const todoStatsAtom = atom((get) => {
  const todos = get(todosAtom);
  const completed = get(completedTodosAtom);
  return {
    total: todos.length,
    completed: completed.length,
    remaining: todos.length - completed.length
  };
});

// Usage in components
function Counter() {
  const [count, setCount] = useAtom(countAtom);
  
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}

function TodoStats() {
  const [stats] = useAtom(todoStatsAtom);
  
  return (
    <div>
      Total: {stats.total}, Completed: {stats.completed}, Remaining: {stats.remaining}
    </div>
  );
}

Server State Management

React Query (TanStack Query)

Excellent for managing server state:

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

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

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <div>Hello, {user.name}!</div>;
}

// Mutations
function UpdateUserForm({ userId }) {
  const queryClient = useQueryClient();
  
  const updateUserMutation = useMutation({
    mutationFn: (userData) => updateUser(userId, userData),
    onSuccess: () => {
      // Invalidate and refetch user data
      queryClient.invalidateQueries(['user', userId]);
    },
  });

  const handleSubmit = (userData) => {
    updateUserMutation.mutate(userData);
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* form fields */}
      <button 
        type="submit" 
        disabled={updateUserMutation.isPending}
      >
        {updateUserMutation.isPending ? 'Updating...' : 'Update'}
      </button>
    </form>
  );
}

SWR

Alternative to React Query:

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then(res => res.json());

function UserList() {
  const { data: users, error, mutate } = useSWR('/api/users', fetcher);

  if (error) return <div>Failed to load</div>;
  if (!users) return <div>Loading...</div>;

  return (
    <div>
      <button onClick={() => mutate()}>Refresh</button>
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}

Choosing the Right Solution

Decision Matrix

Use CaseSolutionWhy
Simple local stateuseStateBuilt-in, simple
Complex local stateuseReducerPredictable updates
Avoiding prop drillingContext APIBuilt-in, no extra deps
Medium app, simple global stateZustandMinimal boilerplate
Large app, complex stateRedux ToolkitMature, excellent devtools
Atomic state managementJotaiFine-grained reactivity
Server stateReact Query/SWRCaching, synchronization

Combination Strategies

You can combine different solutions:

// Global app state with Zustand
const useAppStore = create((set) => ({
  user: null,
  theme: 'light',
  setUser: (user) => set({ user }),
  setTheme: (theme) => set({ theme }),
}));

// Server state with React Query
function App() {
  return (
    <QueryClient client={queryClient}>
      <ThemeProvider>
        <Router>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/profile" element={<Profile />} />
          </Routes>
        </Router>
      </ThemeProvider>
    </QueryClient>
  );
}

// Local state for component-specific data
function TodoForm() {
  const [inputValue, setInputValue] = useState('');
  const addTodoMutation = useMutation({
    mutationFn: createTodo,
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    addTodoMutation.mutate({ text: inputValue });
    setInputValue('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button type="submit">Add Todo</button>
    </form>
  );
}

Best Practices

1. Keep State Close to Where It’s Used

Don’t lift state higher than necessary:

//  Unnecessary global state
function App() {
  const [modalOpen, setModalOpen] = useState(false);
  return (
    <div>
      <Header onOpenModal={() => setModalOpen(true)} />
      <Main />
      {modalOpen && <Modal onClose={() => setModalOpen(false)} />}
    </div>
  );
}

//  Keep modal state local
function Header() {
  const [modalOpen, setModalOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setModalOpen(true)}>Open Modal</button>
      {modalOpen && <Modal onClose={() => setModalOpen(false)} />}
    </div>
  );
}

2. Separate Concerns

Keep different types of state in appropriate places:

  • UI state → Local component state or Context
  • Global app state → Zustand, Redux, or Context
  • Server state → React Query or SWR
  • URL state → React Router or Next.js router

3. Normalize State Structure

Keep state flat and normalized:

//  Nested, denormalized
const state = {
  posts: [
    { id: 1, title: 'Post 1', author: { id: 1, name: 'John' } },
    { id: 2, title: 'Post 2', author: { id: 1, name: 'John' } },
  ]
};

//  Flat, normalized
const state = {
  posts: {
    1: { id: 1, title: 'Post 1', authorId: 1 },
    2: { id: 2, title: 'Post 2', authorId: 1 },
  },
  authors: {
    1: { id: 1, name: 'John' },
  }
};

Conclusion

Modern state management offers many excellent options. Start with React’s built-in solutions and add complexity only when needed. Consider the size of your application, team preferences, and specific requirements when choosing a state management solution.

Remember: the best state management solution is the one that makes your code easier to understand, maintain, and debug.

Table of Contents

Magic UI

Try Magic UI Pro

Beautiful design system

Learn more