Skip to the content.
Agent Skill Available Download this Agent Skill (SKILL.md) to drop into ~/.claude/skills/ or any Agent-Skills-compatible runtime for AI-assisted State Basics work.
Download
state

State Basics

Detailed Description

Frontend state management is a crucial aspect of modern web development, focusing on efficiently handling and organizing data within an application. At its core, state represents any data that can change during the lifetime of your application—it’s what makes your app dynamic and interactive rather than static.

What is State?

State in frontend development refers to any information that an application needs to keep track of over time. This includes:

Types of State

  1. Local State: Managed within individual components
  2. Global State: Shared across multiple components or routes
  3. Server State: Data fetched from external sources that needs synchronization

Core Concepts

Store

A centralized location where the application state is stored, typically as an object.

Properties

Individual data points within the store that represent specific pieces of state.

Actions

Functions or methods used to update properties in the store, similar to setter methods.

The distinction between local, global, and server state is fundamental to effective state management. Local state (component-specific data like input values or dropdown states) should remain close to where it’s used. Global state (data needed across multiple unrelated components like user authentication or app theme) benefits from centralization. Server state (data from APIs) requires synchronization patterns to handle loading, caching, and stale data. Conflating these categories leads to over-complicated components or bloated global stores.

In the Universal Frontend Architecture, state management follows framework-agnostic principles. Whether using Redux, MobX, Vuex, or Pinia, the patterns remain consistent: unidirectional data flow, immutable updates, and separation of state logic from UI components. This consistency enables developers to transfer knowledge across projects and frameworks.

The evolution from manual state management to sophisticated solutions like Redux Toolkit and React Query reflects growing application complexity. Modern apps must handle optimistic updates, offline functionality, real-time synchronization, and complex derived data—challenges that simple setState cannot address. Understanding state fundamentals helps you choose the right tool for each scenario.

Goals of State Management

Key Insight

State is the memory of your application—every piece of data that changes over time, from user inputs to API responses to UI toggles. Mastering state management means understanding when state should live locally in components versus globally in a store, how to keep state synchronized with reality, and how to structure state to make your application predictable, debuggable, and performant.

Code Examples

Basic Example: Local Component State

Simple state management within a single component:

// Counter.js - Local state example

import React, { useState } from 'react';

const Counter = () => {
  // State lives in this component only
  const [count, setCount] = useState(0);
  const [history, setHistory] = useState([]);

  const increment = () => {
    const newCount = count + 1;
    setCount(newCount);
    setHistory([...history, `Incremented to ${newCount}`]);
  };

  const decrement = () => {
    const newCount = count - 1;
    setCount(newCount);
    setHistory([...history, `Decremented to ${newCount}`]);
  };

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      
      <h3>History:</h3>
      <ul>
        {history.map((entry, index) => (
          <li key={index}>{entry}</li>
        ))}
      </ul>
    </div>
  );
};

export default Counter;

Practical Example: Global State with Context API

Sharing state across multiple components:

// ThemeContext.js - Global state with Context

import React, { createContext, useContext, useState } from 'react';

// 1. Create context
const ThemeContext = createContext();

// 2. Create provider component
export const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');
  const [fontSize, setFontSize] = useState(16);

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  const increaseFontSize = () => setFontSize(prev => Math.min(prev + 2, 24));
  const decreaseFontSize = () => setFontSize(prev => Math.max(prev - 2, 12));

  // Expose state and actions
  const value = {
    theme,
    fontSize,
    toggleTheme,
    increaseFontSize,
    decreaseFontSize
  };

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
};

// 3. Create hook for consuming context
export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
};

Usage:

// App.js
import { ThemeProvider } from './ThemeContext';
import Header from './Header';
import Content from './Content';

const App = () => (
  <ThemeProvider>
    <Header />
    <Content />
  </ThemeProvider>
);

// Header.js - Consumes global state
import { useTheme } from './ThemeContext';

const Header = () => {
  const { theme, toggleTheme, fontSize, increaseFontSize, decreaseFontSize } = useTheme();

  return (
    <header style={{ fontSize: `${fontSize}px` }}>
      <h1>Current Theme: {theme}</h1>
      <button onClick={toggleTheme}>Toggle Theme</button>
      <button onClick={increaseFontSize}>A+</button>
      <button onClick={decreaseFontSize}>A-</button>
    </header>
  );
};

Advanced Example: Derived State and Normalization

Complex state management with computed values:

// userState.js - Normalized state structure

const initialState = {
  // Normalized by ID for efficient lookups
  users: {
    byId: {
      '1': { id: '1', name: 'Alice', role: 'admin', departmentId: 'd1' },
      '2': { id: '2', name: 'Bob', role: 'user', departmentId: 'd1' },
      '3': { id: '3', name: 'Charlie', role: 'user', departmentId: 'd2' }
    },
    allIds: ['1', '2', '3']
  },
  departments: {
    byId: {
      'd1': { id: 'd1', name: 'Engineering' },
      'd2': { id: 'd2', name: 'Marketing' }
    },
    allIds: ['d1', 'd2']
  },
  filters: {
    role: null,
    department: null,
    searchTerm: ''
  }
};

// Selectors - Derive state without storing it
const selectAllUsers = (state) => 
  state.users.allIds.map(id => state.users.byId[id]);

const selectFilteredUsers = (state) => {
  let users = selectAllUsers(state);
  const { role, department, searchTerm } = state.filters;

  // Apply role filter
  if (role) {
    users = users.filter(user => user.role === role);
  }

  // Apply department filter
  if (department) {
    users = users.filter(user => user.departmentId === department);
  }

  // Apply search filter
  if (searchTerm) {
    users = users.filter(user => 
      user.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }

  return users;
};

const selectUserStats = (state) => {
  const users = selectAllUsers(state);
  return {
    total: users.length,
    admins: users.filter(u => u.role === 'admin').length,
    byDepartment: users.reduce((acc, user) => {
      acc[user.departmentId] = (acc[user.departmentId] || 0) + 1;
      return acc;
    }, {})
  };
};

export { initialState, selectAllUsers, selectFilteredUsers, selectUserStats };

Common Mistakes

1. Storing Derived Data in State

Mistake: Duplicating data that can be calculated from existing state.

// ❌ BAD: Storing derived data
const [items, setItems] = useState([...]);
const [itemCount, setItemCount] = useState(0); // Redundant!

const addItem = (item) => {
  setItems([...items, item]);
  setItemCount(itemCount + 1); // Easy to forget, causes bugs
};
// ✅ GOOD: Calculate derived data
const [items, setItems] = useState([...]);
const itemCount = items.length; // Always synchronized

const addItem = (item) => {
  setItems([...items, item]); // Single source of truth
};

Why it matters: Derived data creates synchronization bugs and increases complexity. Compute it from source data.

2. Mutating State Directly

Mistake: Modifying state objects instead of creating new ones.

// ❌ BAD: Mutating state
const [user, setUser] = useState({ name: 'Alice', age: 30 });

user.age = 31; // Direct mutation!
setUser(user); // React won't detect the change
// ✅ GOOD: Immutable updates
const [user, setUser] = useState({ name: 'Alice', age: 30 });

setUser({ ...user, age: 31 }); // New object, React detects change

Why it matters: React and most state libraries rely on reference equality to detect changes. Mutations break reactivity and cause subtle bugs.

3. Putting Everything in Global State

Mistake: Making all state global when most should be local.

// ❌ BAD: Global state for component-specific UI
// In Redux store
const state = {
  modal1Open: false,
  modal2Open: false,
  dropdown1Expanded: false,
  inputFocused: false, // UI state that doesn't need to be global
  // ...
};
// ✅ GOOD: Keep UI state local
const Modal = () => {
  const [isOpen, setIsOpen] = useState(false); // Local to this modal
  // ...
};

const Dropdown = () => {
  const [isExpanded, setIsExpanded] = useState(false); // Local
  // ...
};

Why it matters: Global state adds complexity, hurts performance (more components re-render), and makes components less reusable. Only globalize state that’s truly shared. See Store for more details.

Quick Quiz

What are the key differences between local, global, and server state?

Should you always lift state to the highest common ancestor?

Why does state immutability matter?

What is normalised state and when is it useful?

How should you choose between useState, Context, Redux/Pinia/NgRx, and a server-state library?

References