React State Management: From useState to Redux Toolkit
React State Management: From useState to Redux Toolkit
5min
Managing state in React is a core part of building scalable and maintainable applications. While React’s built-in hooks like useState provide a simple and straightforward way to handle local component state, real-world applications often require more sophisticated solutions to handle shared state, avoid prop drilling, and keep the codebase predictable. In this post, we’ll explore the evolution of state management in React, starting with useState and ending with Redux Toolkit.
The Basics: useState
React’s useState hook is often the first state management tool developers learn. It allows you to store and update local state in functional components:
useState works beautifully for state that is local to a component. It’s simple, predictable, and built into React, making it perfect for isolated UI logic like toggles, input fields, and small counters.
The Problem: Prop Drilling and Shared State
As applications grow, state often needs to be shared between components at different levels of the component tree. Passing state and setters through multiple layers of props—commonly called prop drilling—can quickly become cumbersome:
While this works, it adds boilerplate and tightly couples components. Refactoring becomes painful, and adding new layers of nested components can force you to rewire the props chain.
Context API: React’s Built-In Solution
To address prop drilling, React introduced the Context API, which allows you to provide state at a higher level and consume it anywhere in the tree:
1import{ createContext, useState, useContext,ReactNode}from'react';23interfaceCountContextType{4 count:number;5setCount:(value:number)=>void;6}78constCountContext=createContext<CountContextType |undefined>(undefined);910functionCountProvider({ children }:{ children:ReactNode}){11const[count, setCount]=useState<number>(0);12return(13<CountContext.Providervalue={{ count, setCount }}>14{children}15</CountContext.Provider>16);17}1819functionuseCount(){20const context =useContext(CountContext);21if(!context)thrownewError('useCount must be used within CountProvider');22return context;23}2425functionGrandchild(){26const{ count, setCount }=useCount();27return(28<div>29<p>Count: {count}</p>30<buttononClick={()=>setCount(count +1)}>Increment</button>31</div>32);33}
Context is great for moderately complex applications, especially for theme, authentication state, or small shared data. But frequent updates can trigger unnecessary re-renders, and managing multiple contexts can become cumbersome in very large applications.
State Management Libraries: Why They Exist
When state sharing becomes more complex—especially with global state or async data fetching—state management libraries provide solutions that React alone struggles with. Libraries like Zustand, Recoil, and MobX allow you to:
Maintain centralized state outside of components
Reduce or eliminate prop drilling
Handle async logic, caching, and middleware
Integrate with DevTools for debugging
These libraries exist because in real-world applications, React’s hooks and Context API start showing limitations in performance, maintainability, and predictability.
Redux and Redux Toolkit: Modern Redux
Among state management libraries, Redux is the most widely used and battle-tested. Originally, Redux had a lot of boilerplate, which intimidated many developers. Today, Redux Toolkit (RTK) simplifies Redux and provides modern patterns for scalable apps:
Simplified store setup
Immutable updates via createSlice
Async data fetching, caching, and automatic re-fetching via RTK Query
DevTools integration
Here’s a simple counter example using Redux Toolkit:
1import{ configureStore, createSlice,PayloadAction}from'@reduxjs/toolkit';2import{Provider, useDispatch, useSelector }from'react-redux';34const counterSlice =createSlice({5 name:'counter',6 initialState:0,7 reducers:{8increment:(state)=> state +1,9decrement:(state)=> state -1,10addBy:(state, action:PayloadAction<number>)=> state + action.payload,11},12});1314const store =configureStore({ reducer: counterSlice.reducer});1516functionCounter(){17const count =useSelector((state:number)=> state);18const dispatch =useDispatch();1920return(21<div>22<p>Count: {count}</p>23<buttononClick={()=>dispatch(counterSlice.actions.increment())}>Increment</button>24<buttononClick={()=>dispatch(counterSlice.actions.addBy(5))}>Add 5</button>25</div>26);27}2829functionApp(){30return(31<Providerstore={store}>32<Counter/>33</Provider>34);35}
RTK Query Features
RTK Query is a powerful addition to Redux Toolkit that handles server state efficiently. Key features include:
Data fetching with caching out of the box
Automatic re-fetching on cache invalidation or parameter changes
Optimistic updates for smoother UX
Auto-generated hooks for queries and mutations
Built-in TypeScript support for request and response types
Using RTK Query, you don’t need to manually manage loading states, caching, or error handling—it’s all integrated and type-safe.
Conclusion
State management in React evolves as your app grows. Start simple with useState for local state. Move to Context API when sharing state becomes necessary. And for large, complex apps with global state, async data, and caching needs, Redux Toolkit with RTK Query provides a modern, maintainable, and scalable solution. Understanding these tools ensures your React applications remain performant and predictable.