State Management in React: Modern Patterns and Best Practices

Explore modern React state management solutions including Context API, Zustand, Jotai, and when to use each for different application needs.

3 min read
By Sagar Tank
ReactState ManagementContext APIZustandRedux
S
Image
State Management in React: Modern Patterns and Best Practices

State management is crucial for building scalable React applications. Understanding when and how to manage state effectively can make or break your application's architecture.

Local State (useState)

For component-specific state:

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

Context API

For shared state across components:

// Create context
const ThemeContext = createContext<{
  theme: 'light' | 'dark'
  toggleTheme: () => void
}>({
  theme: 'light',
  toggleTheme: () => {},
})

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

// Hook
export function useTheme() {
  return useContext(ThemeContext)
}

Zustand (Lightweight State Management)

Perfect for medium-sized applications:

import { create } from 'zustand'

interface Store {
  count: number
  increment: () => void
  decrement: () => void
}

const useStore = create<Store>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}))

// Usage
function Counter() {
  const { count, increment, decrement } = useStore()
  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  )
}

Server State (React Query / SWR)

For server data:

import { useQuery } from '@tanstack/react-query'

function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  })
  
  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
  
  return <div>{data.name}</div>
}

State Management Patterns

1. Lift State Up

Move shared state to the nearest common ancestor:

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

2. Composition over Context

Prefer composition when possible:

function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <Routes />
      </UserProvider>
    </ThemeProvider>
  )
}

3. Colocate State

Keep state close to where it's used:

// Good: State close to usage
function UserProfile() {
  const [expanded, setExpanded] = useState(false)
  // ...
}

// Bad: Unnecessary lifting
function App() {
  const [userExpanded, setUserExpanded] = useState(false)
  // ...
}

When to Use What

  • useState: Component-specific state
  • Context API: Theme, user preferences, shared UI state
  • Zustand: Medium complexity, multiple stores
  • Redux: Large applications, complex state logic
  • React Query: Server state, caching, synchronization

Best Practices

  1. Minimize state - Don't store derived values
  2. Normalize state - Keep data structure flat
  3. Use selectors - Prevent unnecessary re-renders
  4. Memoize callbacks - Use useCallback for stable references
  5. Split contexts - Don't put everything in one context

Conclusion

Choosing the right state management solution depends on your application's needs. Start simple with useState and Context API, then add libraries like Zustand or React Query as complexity grows.

Enjoyed this article?

Check out more articles on the blog or get in touch to discuss your project.