TechLead
πŸ”„
Intermediate
6 min read

State Management

Redux, Context API, and state management patterns

State management questions in frontend interviews test whether you understand when to reach for each tool, not just how to use them. The goal is to keep state as local as possible and reach for shared state solutions only when genuinely needed.

Types of State

  • Local UI state: open/closed, hover, form input β€” useState
  • Shared component state: lifting state up, or Context for moderate trees
  • Server state: async data from APIs β€” TanStack Query, SWR, RTK Query
  • URL state: filters, pagination, current view β€” useSearchParams
  • Global client state: auth, theme, user preferences β€” Zustand, Redux

useState and Derived State

// Common mistake: duplicating derived state
const [items, setItems] = useState([]);
const [count, setCount] = useState(0); // BAD β€” count is just items.length

// Correct: derive it
const [items, setItems] = useState([]);
const count = items.length; // computed from source of truth

// Functional updates for state that depends on previous state
setItems(prev => [...prev, newItem]);    // safe in async contexts
setCount(prev => prev + 1);

Context β€” Right and Wrong Uses

Context is not a performance-optimised state manager. Every consumer re-renders when context value changes. Use it for infrequently updated values (theme, auth, locale); avoid it for high-frequency updates like form state.

// Optimise Context to avoid unnecessary re-renders
// 1. Split context by update frequency
const ThemeContext = createContext(); // updates rarely
const CartContext  = createContext(); // updates often β€” consider Zustand instead

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

Zustand β€” Minimal Global State

import { create } from 'zustand';

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

// In a component β€” only re-renders when items changes
const items = useCartStore(state => state.items);
const addItem = useCartStore(state => state.addItem);

Server State β€” TanStack Query

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

function UserProfile({ userId }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch('/api/users/' + userId).then(r => r.json()),
    staleTime: 5 * 60 * 1000, // consider fresh for 5 minutes
  });

  const queryClient = useQueryClient();
  const mutation = useMutation({
    mutationFn: (updates) => fetch('/api/users/' + userId, { method: 'PATCH', body: JSON.stringify(updates) }),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['user', userId] }),
  });
}

Interview Decision Framework

  • Can it stay in useState locally? β†’ use it
  • Needed by a sibling? β†’ lift state to common parent
  • Needed deeply in the tree but changes infrequently? β†’ Context
  • Needed deeply and changes often? β†’ Zustand or Redux Toolkit
  • Is it data from an API? β†’ TanStack Query or SWR, not Redux
  • Can it live in the URL? β†’ URL params, bookmarkable and shareable

Continue Learning