💼 React State Management

React state management is the most essential decision for entrepreneurs to build scalable, performant, and robust React applications. It ensures that our app remains in sync with the user interface.


Introduction

What is State?

State is an object that holds data which changes over time in an application. In React, state provides a mechanism for components to manage and track changing data, triggering re-renders when updated.

When to Use State

In React apps, components display or operate on data—such as user input, API responses, or other dynamic content. When you need to store and update data inside a component, use state.

Why is State Management Important?

State represents the data that changes over time in your application. Proper state management ensures your app functions correctly, helps avoid data inconsistency, prop drilling, and unnecessary re-renders. As your application grows, managing state efficiently becomes critical for maintainability, scalability, and performance.


Different Approaches to Managing State in React

React state can be categorized into several types:

  • Local (UI) state: Managed within a single component.
  • Global state: Shared across multiple components.
  • Server state: Data fetched from an external server, often with caching and synchronization.
  • URL state: Data stored in URLs, such as query parameters and route params.

Component (Local) State

Each React component has its own internal state, managed with the useState hook. This state is specific to the component and updated with setState (or setCount, etc.), triggering a re-render.

import { useState } from 'react'
function Counter() {
  const [count, setCount] = useState(0)
  const increment = () => setCount(count + 1)
  const decrement = () => setCount(count - 1)
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}

Local state is great for isolated data, but sharing state between many components can become challenging.

Global State

Global state is accessible to every component in the application. It is typically declared in a root or provider component. Use global state when multiple components need to access or update the same data, such as authentication, user preferences, or theme.

React Context API

React’s Context API is a built-in solution for sharing state across components without prop drilling. It's ideal for smaller apps or sharing state between a few related components. However, overusing Context can lead to performance issues, as it is not optimized for high-frequency updates.

As Sebastian Markbage notes, Context is best for low-frequency updates like themes or authentication.

Example:

import { createContext, useContext, useState } from 'react'
 
const ThemeContext = createContext()
 
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}
 
function ThemedButton() {
  const { theme, setTheme } = useContext(ThemeContext)
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Current theme: {theme}
    </button>
  )
}

React State Management Libraries

As applications grow, you may need more advanced state management. Libraries can be grouped as follows:

  • Reducer-based: Centralized state, updated via dispatched actions (e.g., Redux, Zustand).
  • Atom-based: State is split into small, independent atoms (e.g., Recoil, Jotai).
  • Mutable-based: Uses proxies for mutable, reactive state (e.g., MobX, Valtio).

Reducer-based Libraries

Redux Flow

Redux & Redux Toolkit

Redux is a classic state container for React. State changes only via dispatched actions, handled by reducers. Redux Toolkit simplifies setup and reduces boilerplate.

import { createSlice, configureStore } from '@reduxjs/toolkit'
const exampleSlice = createSlice({
  name: 'example',
  initialState: { isAvailable: true },
  reducers: {
    makeAvailable: (state) => {
      state.isAvailable = true
    },
    makeUnavailable: (state) => {
      state.isAvailable = false
    },
  },
})
const store = configureStore({ reducer: { example: exampleSlice.reducer } })
// Usage in components:
const isAvailable = useSelector((state) => state.example.isAvailable)
const dispatch = useDispatch()
dispatch(exampleSlice.actions.makeAvailable())
Rematch

Rematch is a Redux abstraction that reduces boilerplate and simplifies async logic.

const countModel = {
  state: 0,
  reducers: {
    increment: (state, payload) => state + payload,
  },
  effects: (dispatch) => ({
    async incrementAsync(payload) {
      await new Promise((resolve) => setTimeout(resolve, 1000))
      dispatch.count.increment(payload)
    },
  }),
}

Zustand

Zustand is a minimal, hook-based state management library.

import { create } from 'zustand'
const useStore = create((set, get) => ({
  isAvailable: true,
  status: () => (get().isAvailable ? 'Available' : 'Unavailable'),
  makeAvailable: () => set({ isAvailable: true }),
  makeUnavailable: () => set({ isAvailable: false }),
}))
const { isAvailable, makeAvailable } = useStore()

You can also combine Immer with Zustand to handle nested objects and complicated data.

Atom-based Libraries

Recoil Flow

Recoil

Recoil introduces atoms (units of state) and selectors (derived/computed state).

import { atom, selector, useRecoilState, useRecoilValue } from 'recoil'
const isAvailableState = atom({ key: 'isAvailableState', default: true })
const statusState = selector({
  key: 'statusState',
  get: ({ get }) => (get(isAvailableState) ? 'Available' : 'Unavailable'),
})
const [isAvailable, setIsAvailable] = useRecoilState(isAvailableState)
const status = useRecoilValue(statusState)

Jotai

Jotai is a minimal atomic state library using JavaScript's WeakMap for efficient memory management.

import { atom, useAtom } from 'jotai'
const isAvailableState = atom(true)
const statusState = atom((get) =>
  get(isAvailableState) ? 'Available' : 'Unavailable',
)
const [isAvailable, setIsAvailable] = useAtom(isAvailableState)
const [status] = useAtom(statusState)

Mutable-based Libraries

Mobx Flow

MobX

MobX uses observables and OOP patterns for state management.

import { makeObservable, observable, computed } from 'mobx'
class TodoList {
  todos = []
  get unfinishedTodoCount() {
    return this.todos.filter((todo) => !todo.finished).length
  }
  constructor(todos) {
    makeObservable(this, {
      todos: observable,
      unfinishedTodoCount: computed,
    })
    this.todos = todos
  }
}
import { observer } from 'mobx-react-lite'
const TodoListView = observer(({ todoList }) => (
  <div>
    <ul>
      {todoList.todos.map((todo) => (
        <TodoView todo={todo} key={todo.id} />
      ))}
    </ul>
    Tasks left: {todoList.unfinishedTodoCount}
  </div>
))

Valtio

Valtio uses proxies for fine-grained reactivity.

import { proxy, useSnapshot } from 'valtio'
const state = proxy({ count: 0, text: 'hello' })
function Counter() {
  const snap = useSnapshot(state)
  return (
    <div>
      {snap.count}
      <button onClick={() => ++state.count}>+1</button>
    </div>
  )
}

Server State and Data Fetching

Modern React apps often need to manage server state—data fetched from APIs that must be cached, synchronized, and updated. Libraries like React Query, SWR, and Relay help manage server state, caching, background updates, and optimistic UI.

Example with React Query:

import { useQuery } from '@tanstack/react-query'
function Todos() {
  const { data, isLoading } = useQuery(['todos'], fetchTodos)
  if (isLoading) return <div>Loading...</div>
  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
}

URL State

URL state includes data stored in the URL, such as query parameters, route params, and hash fragments. This is useful for filters, pagination, and sharing links. Use libraries like React Router or Next.js router to manage URL state.


Conclusion: Choosing the Right State Management Approach

The best state management approach depends on your app's complexity and your team's needs:

  • For simple apps, useState and useReducer may suffice.
  • For global or complex state, consider Context, Zustand, MobX, or atomic libraries like Recoil and Jotai.
  • For server state, use React Query, SWR, or Relay.
  • For URL state, leverage React Router or Next.js router.

Evaluate your application's needs, the scale of state changes, and future growth potential. As React evolves, new state management tools and patterns emerge. Atomic state management is gaining traction for modularity and performance. Ultimately, choose the approach that makes your code more readable, maintainable, and scalable.

Managing state between components is fundamental in React. By understanding the available methods and tools, you can build robust, efficient applications. Stay informed about the latest best practices to maximize React's capabilities.


Further Reading