DEV Community

Rizwan Saleem
Rizwan Saleem

Posted on

How to manage state in modern frontend applications: a practical guide

How to manage state in modern frontend applications: a practical guide

Frontend state management tutorial: a practical guide across local, server, URL, and app state with concrete patterns and code.

What to manage where

  • Local UI state: useState, useReducer, or component-level hooks for transient UI like form inputs, toggles, modals.
  • Server data state: fetchable data from APIs that needs caching, invalidation, and background refresh.
  • URL state: values encoded in the URL (query params) to enable shareable/bookmarkable state.
  • App/global state: cross-cutting state shared across many components, like authentication, theme, or user preferences.
  • Persisted client state: state that should survive page reloads, e.g., user preferences saved to localStorage.

Patterns and when to use them

  • Context API
    • When to use: simple global state that doesn’t need complex logic or heavy updates; you want to avoid prop drilling without adding a full library.
    • Pros: built-in to React, minimal setup, good for lightweight sharing.
    • Cons: can become hard to manage for large state; re-renders can cascade if not careful.
    • How to implement: create a context, provide a value object, and consume with useContext or a custom hook.
  • Redux
    • When to use: large apps with complex data flows, strict immutability, time-travel debugging, or when multiple teams own different domains of state.
    • Pros: predictable state container, robust ecosystem, great devtools.
    • Cons: boilerplate, learning curve, can be overkill for small apps.
    • Variants: classic Redux with reducers and actions; Redux Toolkit for ergonomic patterns.
  • Zustand
    • When to use: lightweight, fast setup, desire for simple store access outside React components; good for small-to-medium apps.
    • Pros: minimal boilerplate, fine-grained reactivity, easy to use outside React.
    • Cons: smaller ecosystem than Redux; middleware patterns exist but are different.
  • React Query / TanStack Query
    • When to use: server state management-data fetched from APIs that benefits from caching, background refreshing, refetching, and synchronization.
    • Pros: reduces boilerplate for fetching and caching; handles stale data and consistency.
    • Cons: not a general state store; focus is on server state, not client UI state.
  • URL parameters (search params)
    • When to use: shareable or bookmarkable app state; filter/sort/pagination states that users might want to persist via URL.
    • Pros: enables deep linking; easy to copy/share state.
    • Cons: URL length limits; encoding/decoding boilerplate; can expose sensitive data if not careful.
  • URL state patterns to avoid over-engineering
    • Keep only essential pieces in the URL (e.g., current page, active filters, search query) while keeping heavy UI state in memory.
    • Synchronize selectively: update URL on user-initiated changes, debounce or throttle if updates are frequent.

Choosing the right combination

  • Start with the simplest tool that fits the need.
  • Separate concerns: treat server/state data with a dedicated tool (React Query), UI/local state with React hooks, and cross-cutting, non-UI state with Context or a small store.
  • For large apps, consider a layered approach: local components + Context for light global share + a dedicated store for domain-wide state + React Query for server data + URL params for shareable filters.

Architectural patterns

  • Global store with domain slices
    • Structure: split the store into domain-specific slices (e.g., user, projects, UI). This makes maintenance easier and reduces unnecessary re-renders.
    • Example approach: with a library like Zustand or Redux Toolkit, create separate slices and compose them.
  • Server-state-first pattern
    • Use a dedicated server-state manager (React Query) for all API data. Keep component-level caches in sync; derive UI state (like loading status) from React Query’s hooks.
  • URL-driven UI
    • Map key UI controls to URL search params: page, sort, filter, query. Read from URL on initial load and write back on changes. Use a router utility to keep the URL in sync.

Real-world example: a product list page

  • Local state: search input, current modal visibility
  • URL state: page number, page size, sort field, price range filters
  • Server state: product data, category data fetched via API
  • App state: user authentication status, cart contents

Minimal working code snippets

  • Local UI state with useState
    • const [query, setQuery] = useState('');
    • const [modalOpen, setModalOpen] = useState(false);
  • Context API for simple global UI state
    • Create a ThemeContext to toggle light/dark mode and provide a useTheme hook.
  • Zustand store for cross-cutting app state
    • Create a small store with user, theme, and a simple toggle; export hooks for usage in components.
  • React Query for server data
    • useQuery(['products', filters], fetchProducts)
    • useMutation for actions that modify server data (e.g., add to cart)
  • URL params with TanStack Router or direct URL API
    • Read: new URLSearchParams(window.location.search).get('page')
    • Write: update the URL via history.pushState or a router’s navigation method
  • Testing state logic
    • Unit tests: test reducers or pure state functions with deterministic inputs.
    • Integration tests: render components with mocked contexts and server responses; verify state transitions and UI changes.
    • For server state: test caching and invalidation by simulating API responses and refetch logic.

Anti-patterns to avoid

  • Storing everything in a single global store without structure
  • Overusing Context for large state updates that cause frequent re-renders
  • Mixing server and client state in the same store without clear boundaries
  • Relying on URL state for non-shareable ephemeral UI state (noisy URLs)

Testing tips

  • Isolate state logic: test pure functions and reducers separately.
  • Mock network calls when testing server-state hooks.
  • Test URL synchronization: simulate URL changes and verify UI reflects the state correctly; also verify that state changes update the URL predictably.

Tooling quick-start checklist

  • Start with React hooks for local UI state
  • Add Context for lightweight global state needs
  • Introduce a small store (Zustand or Redux Toolkit) for domain-level state
  • Introduce React Query for server data
  • Implement URL-driven state for shareable filters and navigation
  • Add tests early for state logic and URL synchronization

Illustration example: state flow

  • UI action (type a query) -> local state updates -> URL params update -> React Query fetchs new server data -> UI renders with new data
  • This keeps the user experience responsive while ensuring shareable, consistent state across sessions.

Would you like a compact, copy-paste starter project that wires up a sample page (local form state, a global theme via Context, server data with React Query, and URL-driven filters) with a small test suite?
If so, I can tailor it to your stack (Create React App, Next.js, or Vite) and include ready-to-run snippets for each pattern.

Possible clarifying directions

  • Do you prefer a Next.js or a plain React setup for this tutorial
  • Are you targeting a particular app domain (e-commerce, dashboard, social app)
  • Do you want TypeScript included by default

-

Rizwan Saleem | https://rizwansaleem.co

Top comments (0)