Skip to main content
โšก Calmops

State Management in Frontend: A Complete Guide

State management is one of the most challenging aspects of building complex frontend applications. This guide covers everything you need to know about managing state in modern web applications.

Types of State

Local State

Local state lives in a single component:

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

Component State

State shared between components:

function Parent() {
  const [value, setValue] = useState('');
  
  return (
    <>
      <Input value={value} onChange={setValue} />
      <Display value={value} />
    </>
  );
}

Application State

Global state shared across the app:

// Redux store
const store = createStore(reducer);

function App() {
  const state = useSelector(state => state);
  const dispatch = useDispatch();
  
  return <div>{state.user.name}</div>;
}

Local State Patterns

useState Best Practices

// Simple state
const [count, setCount] = useState(0);

// Complex state - use reducer
const [state, dispatch] = useReducer(reducer, initialState);

// Lazy initialization
const [data, setData] = useState(() => {
  return expensiveInitialCalculation();
});

// Functional updates
setCount(prev => prev + 1);

useReducer for Complex State

const initialState = {
  user: null,
  loading: false,
  error: null
};

function reducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, user: action.payload };
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
}

function UserProfile() {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  useEffect(() => {
    dispatch({ type: 'FETCH_START' });
    fetchUser()
      .then(user => dispatch({ type: 'FETCH_SUCCESS', payload: user }))
      .catch(err => dispatch({ type: 'FETCH_ERROR', payload: err.message }));
  }, []);
  
  if (state.loading) return <Spinner />;
  if (state.error) return <Error message={state.error} />;
  return <div>{state.user.name}</div>;
}

Global State Solutions

Context API

// Create context
const ThemeContext = createContext();

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

// Consumer
function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <header className={theme}>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </header>
  );
}

Redux

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

const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [], total: 0 },
  reducers: {
    addItem: (state, action) => {
      state.items.push(action.payload);
      state.total += action.payload.price;
    },
    removeItem: (state, action) => {
      const index = state.items.findIndex(item => item.id === action.payload);
      if (index !== -1) {
        state.total -= state.items[index].price;
        state.items.splice(index, 1);
      }
    }
  }
});

export const { addItem, removeItem } = cartSlice.actions;
export const store = configureStore({ reducer: { cart: cartSlice.reducer } });

// Usage with hooks
function Cart() {
  const { items, total } = useSelector(state => state.cart);
  const dispatch = useDispatch();
  
  return (
    <div>
      {items.map(item => (
        <div key={item.id}>
          {item.name} - ${item.price}
          <button onClick={() => dispatch(removeItem(item.id))}>
            Remove
          </button>
        </div>
      ))}
      <div>Total: ${total}</div>
    </div>
  );
}

Zustand

import { create } from 'zustand';

const useStore = create((set) => ({
  user: null,
  setUser: (user) => set({ user }),
  logout: () => set({ user: null })
}));

function Profile() {
  const { user, logout } = useStore();
  
  return user ? (
    <div>
      <p>Welcome, {user.name}</p>
      <button onClick={logout}>Logout</button>
    </div>
  ) : (
    <p>Please login</p>
  );
}

Jotai (Atomic State)

import { atom, useAtom } from 'jotai';

// Primitive atoms
const countAtom = atom(0);
const userAtom = atom(null);

// Derived atoms
const doubleCountAtom = atom((get) => get(countAtom) * 2);

// Compound atom
const userNameAtom = atom(
  (get) => get(userAtom)?.name ?? 'Guest'
);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const [doubleCount] = useAtom(doubleCountAtom);
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Double: {doubleCount}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

Server State

React Query (TanStack Query)

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

function UserList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(res => res.json()),
    staleTime: 5 * 60 * 1000, // 5 minutes
    retry: 3
  });
  
  if (isLoading) return <Spinner />;
  if (error) return <Error error={error} />;
  
  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

function AddUser() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: (newUser) => 
      fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(newUser)
      }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    }
  });
  
  return (
    <form onSubmit={mutation.mutate}>
      <input name="name" />
      <button type="submit" disabled={mutation.isPending}>
        Add User
      </button>
    </form>
  );
}

SWR

import useSWR from 'swr';

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher);
  
  if (isLoading) return <Spinner />;
  if (error) return <Error />;
  
  return <div>{data.name}</div>;
}

State Management Best Practices

Do’s

// DO: Keep state as local as possible
function Form() {
  const [value, setValue] = useState('');
  // Only lift state if needed by siblings
}

// DO: Use appropriate tool for the job
// - useState: simple local state
// - useReducer: complex state logic
// - Context: theme, auth, locale
// - Redux/Zustand: complex global state
// - React Query: server state

Don’ts

// DON'T: Over-engineer with Redux for simple apps
// If you have 5 components sharing state, maybe context is enough

// DON'T: Put everything in global state
// Keep derived/computed values as selectors

// DON'T: Forget to normalize nested state
// Flat state is easier to update

Performance Optimization

Memoization

// useMemo for expensive calculations
const filteredItems = useMemo(() => {
  return items.filter(item => item.name.includes(search));
}, [items, search]);

// useCallback for function references
const handleClick = useCallback((id) => {
  dispatch({ type: 'SELECT', payload: id });
}, [dispatch]);

// React.memo for component memoization
const ListItem = React.memo(function ListItem({ item, onClick }) {
  return <li onClick={() => onClick(item.id)}>{item.name}</li>;
});

Selector Pattern

// Bad: Select entire state
const user = useSelector(state => state);

// Good: Select only needed data
const userName = useSelector(state => state.user.name);
const userAvatar = useSelector(state => state.user.avatar);

// Great: Use memoized selectors
const selectUser = createSelector(
  [state => state.user],
  user => user
);

const userName = useSelector(state => selectUser(state).name);

Summary

Choose the right state management approach:

  • Local State: useState, useReducer
  • Shared Component State: Context
  • Global App State: Redux, Zustand, Jotai
  • Server State: React Query, SWR
  • URL State: React Router

Key principles:

  • Keep state as local as possible
  • Use appropriate tools for the job
  • Normalize complex state
  • Memoize expensive operations
  • Derive state when possible

Build robust state management into your applications!

Comments