Editor’s note: This Redux vs. React Hooks comparison article was last updated on 23 June 2023 to consider how effective the React Redux Toolkit is for state management. Read more about Redux Toolkit here.
Redux Toolkit simplifies the use of Redux in our application. However, by using the React Context API, in addition to the useContext and useReducer Hooks, which were introduced in React v16.8, we can implement robust state management and avoid prop drilling without the need to import additional libraries.
This results in a fairly effective solution for state management in React applications. And yet, far too many React developers default to Redux for state management without considering the alternatives. In this tutorial, we’ll introduce you to the React Context API for state management and explain how React Hooks and the Context API can replace Redux.
If you’re more of a visual learner, the video below describes the React Context API and offers some reasons why you should replace Redux with React Hooks and Context:
No Title
Try LogRocket for free: https://logrocket.com/?yt5 You don’t really need Redux. In this video, we are going to learn how you can get a lot of the same benefits that you would get from Redux using two Hooks that come with React. 0:00 Introduction 0:40 How React Redux works?
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
To handle data between disconnected components in React, developers use prop drilling. There is no global state that components can access. Therefore, if you want to pass data from a top-level component to, for instance, a fifth-level component, you’ll have to pass the data as a prop on each level of the tree until you get to your desired component.
This results in writing a ton of extra code, and giving components properties that they will never use also affects their architectural design. To solve this problem, we need to provide a global state that all components can access, no matter how deeply nested they are.
By solving this problem, Redux, an open source JavaScript library for managing application state, became the go-to solution for React developers.
To some extent, Redux works well for state management in React applications and has a few advantages. However, its verbosity makes it difficult to learn, and the extra code needed to get it working can introduce unnecessary complexity.
One alternative to consider when evaluating state management options is Redux Toolkit. Redux Toolkit is a library that provides a simplified and opinionated approach to working with Redux. It addresses some of the challenges associated with traditional Redux and offers a more streamlined development experience.
On the other hand, with React Hooks and the useContext API, there is no need to install external libraries or add a bunch of files and folders to make our app work. This makes it a much simpler, more straightforward approach to handling global state management in React applications.
Let’s take a closer look at Redux, Redux Toolkit, React Hooks, and the Context API to see how they work, what challenges developers face when using these tools, the scenarios in which each option is suitable, and how using React Hooks and Context can help you overcome some common issues associated with Redux.
Redux is a predictable state container for JavaScript applications that helps us write applications that behave consistently, run in different environments, and are easy to test.
One disadvantage of prop drilling is that it requires us to write a considerable amount of extra code to access data from a top-level component. With Redux, this disadvantage becomes more severe because it requires additional code to set up a global state. Redux requires three main building parts to function: actions, reducers, and store.
Actions are objects that are used to send data to the Redux store. They typically have two properties: a type property for describing what the action does, and a payload property that contains the information that should be changed in the app state:
// action.js
const reduxAction = payload => {
return {
type: 'action description',
payload
}
};
export default reduxAction;
type is usually in all caps with its words separated by underscores. For example, SIGNUP_USER or DELETE_USER_DATA.
Reducers are pure functions that implement the action’s behavior. They take the current application state, perform an action, and then return a new state:
const reducer = (state, action) => {
const { type, payload } = action;
switch(type){
case "action type":
return {
["action description"]: payload
};
default:
return state;
}
};
export default reducer;
The application’s state is housed in the store. There is only one store in any Redux application:
import { createStore } from 'redux'
const store = createStore(componentName);
Because our application can only have one Redux store, to create a single root reducer for all our components, we’ll need the combineReducers method from Redux. With the considerable amount of code required to set up Redux, imagine what our codebase would look like when we have multiple components to work with.
Even though Redux solves our state management problem, it is time-consuming to use, has a difficult learning curve, and introduces a whole new layer of complexity to our application. Fortunately, the React Context API solves these issues. When combined with React Hooks, we have a state management solution that is less time-consuming to set up, has an easier learning curve, and requires minimal code.
To address the complexity and verbosity of Redux, the Redux team introduced Redux Toolkit, a recommended package that simplifies working with Redux and reduces boilerplate code. Redux Toolkit provides several utilities and abstractions to streamline the Redux workflow:
createSlice: This utility combines the actions and reducers into a single slice of the Redux state. It automatically generates action creators and reducer functions, reducing the amount of boilerplate code neededconfigureStore: This function configures the Redux store with sensible defaults. It includes built-in middleware, such as Redux Thunk for handling asynchronous logic, and automatically sets up the Redux DevTools Extension for debuggingcreateAsyncThunk: This utility simplifies handling asynchronous operations in Redux, such as API calls. It generates asynchronous action creators that dispatch multiple actions to reflect the different stages of the async operation (e.g., loading, success, failure)createEntityAdapter: This API helps manage normalized data structures in the Redux store. It provides utility functions for working with entities and collections, making it easier to handle CRUD operationsBy leveraging Redux Toolkit, developers can write Redux code in a more concise and intuitive manner, reducing the overall complexity and boilerplate associated with traditional Redux setup. It improves developer productivity and enhances the maintainability of Redux applications.
When to consider Redux Toolkit:
It’s important to note that Redux Toolkit is still based on Redux principles and uses the same underlying concepts. However, it aims to simplify the development experience and address some of the pain points associated with manual Redux setup.
In comparison, React Hooks such as useContext and useReducer, combined with the Context API, offer a simpler and more lightweight approach to state management. They are particularly suitable for smaller applications or cases where the complexity of Redux might not be necessary.
By understanding the trade-offs and considering the specific needs of your application, you can make an informed decision about whether to use Redux, Redux Toolkit, or React Hooks with the Context API for your state management solution.
The Context API was introduced in React v16.3. React Context enables you to share data that can be considered global for a tree of React components, like the current authenticated user, theme, or preferred language.
Context provides a way to pass data through the component tree without having to pass props down manually at every level. Essentially, the React Context API is React’s way of managing state in multiple components that are not directly connected.
To create a context, we’ll use the createContext method from React, which accepts a parameter for its default value:
import {createContext} from 'react';
const newContext = createContext({ color: 'black' });
The createContext method returns an object with a Provider and a Consumer component:
const { Provider, Consumer } = newContext;
The Provider component makes the state available to all child components, no matter how deeply nested they are within the component hierarchy. The Provider component receives a value prop, which is where we’ll pass our current value:
<Provider value={color: 'blue'}>
{children}
</Provider>
The Consumer, as its name implies, consumes the data from the Provider without any need for prop drilling:
<Consumer>
{value => <span>{value}</span>}}
</Consumer>
Without Hooks, the Context API might not seem like much when compared to Redux. But, when combined with the useReducer Hook, we have a solution that finally solves the state management problem in React.
Hooks are functions that enable the execution of custom code in React’s functional components. They allow us to hook into React’s core features and handle state management easily.
React Hooks provide an alternative to writing class-based components by allowing us to easily handle state management from functional components. Check out this handy React Hooks cheat sheet.
useContext HookYou may have noticed that when describing the React Context API, we needed to wrap our content in a Consumer component, then pass a function as a child so that we could access or consume our state.
Doing so introduces unnecessary component nesting and increases the complexity of our code. The useContext Hook makes things a lot cleaner and more straightforward. To access our state with the useContext Hook, we simply need to call it with our created context as its argument:
import { createContext, useContext } from "react";
const newContext = createContext({ color: 'black' });
const value = useContext(newContext);
console.log(value); // this will return { color: 'black' }
Now, instead of wrapping our content in a Consumer component, we can simply access our state through the value variable.
useReducer HookThe useReducer Hook came with React v16.8. Just like the reduce() method in JavaScript, the useReducer Hook receives two values as its argument, a reducer function and an initial state. Then, it returns a new state:
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + 1
};
case 'DECREMENT':
return {
count: state.count - 1
};
case 'RESET':
return {
count: 0
};
default:
throw new Error('Unknown action');
}
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
}
In the code block above, we’ve defined our state and a corresponding method, dispatch, to handle it. When we call the dispatch method, the useReducer()Hook will perform an action based on the type that our method receives in its action argument:
// ...
return (
<>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</>
)
When the button is clicked, it triggers the dispatch function with the specified action type, which will be handled by the reducer function to update the state accordingly. Check out this example in CodeSandbox.
useReducer Hook with React ContextTo achieve optimal global state management in our React application, we can combine the power of the Context API and the useReducer Hook. To set it up, we’ll first create our global state in a store.js file:
// store.js
import { createContext, useReducer } from "react";
const initialState = {
color: "red"
};
const store = createContext(initialState);
const { Provider } = store;
const StateProvider = ({ children }) => {
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case "CHANGE_COLOR":
return { ...state, color: action.payload };
default:
throw new Error();
}
}, initialState);
return <Provider value={{ state, dispatch }}>{children}</Provider>;
};
export { store, StateProvider };
In our store.js file, we used the createContext() method from React to create a new context. Remember that the createContext() method returns an object with a Provider and Consumer component. This time, we’ll only use the Provider component and the useContext Hook when we need to access our state.
Notice how we used the useReducer Hook in our StateProvider. When we need to manipulate our state, we’ll call the dispatch method and pass in an object with the desired type as its argument.
In our StateProvider, we returned our Provider component with a value prop of state and dispatch from the useReducer Hook.
To access our state globally, we’ll need to wrap our root <App/> component in our StateProvider before rendering it in our ReactDOM.render() function:
// index.js
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import { StateProvider } from "./store";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<StateProvider>
<App />
</StateProvider>
</StrictMode>
);
Now, our store context can be accessed from any component in the component tree. To do so, we’ll import the useContext Hook from React and the store from our ./store.js file:
// ExampleComponent.js
import { useContext } from 'react';
import { store } from './store.js';
const ExampleComponent = () => {
const globalState = useContext(store);
console.log(globalState); // this will return { color: red }
};
We’ve seen how we can access our global state. To add and remove data from our state, we’ll need the dispatch method from our store context. We only need to call the dispatch method and pass in an object with type as its parameter, the action description as defined in our StateProvider component:
// ExampleComponent.js
import { useContext } from 'react';
import { store } from './store.js';
const ExampleComponent = () => {
const { state, dispatch } = useContext(store);
const changeColor = () => {
dispatch({ type: "CHANGE_COLOR", payload: "blue" });
};
return (
<div>
<h2>Example Component</h2>
<p>Current color: {state.color}</p>
<button onClick={changeColor}>Change Color</button>
</div>
);
};
export default ExampleComponent;
Check out this example in CodeSandbox.
| Context API | Redux | |
|---|---|---|
| Built-in tool | Ships with React | Requires additional installation |
| Setup | Minimal setup required | Extensive setup required |
| Data type | Suitable for static data | Works with static and dynamic data |
| Extensibility | Creation of new contexts from scratch | Easily extendible with new data/actions |
| Debugging | Debugging can be challenging | Powerful Redux Dev Tools available |
| Code organization | UI logic and state management in the same component | Separation of UI and state management logic |
It’s important to consider the complexity and specific requirements of your project when deciding whether to use Redux or the Context API. Both approaches have their strengths and choosing the right one will depend on the scale, nature, and goals of your application.
In this tutorial, we explored the differences between using Redux for state management in React apps and using the React Context API with the useContext and useReducer Hooks.
When we use Redux for state management, we have to deal with prop drilling, meaning we have to write a lot of extra code just to get our application started. By using Context to nest components within components, all the functionality from the parent component is available in the child components.
I hope you enjoyed this article, and be sure to leave a comment if you have any questions.
Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not
server-side
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>

This article showcases a curated list of open source mobile applications for Flutter that will make your development learning journey faster.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the April 1st issue.

This post walks through a complete six-step image optimization strategy for React apps, demonstrating how the right combination of compression, CDN delivery, modern formats, and caching can slash LCP from 8.8 seconds to just 1.22 seconds.

Learn what vinext is, how Cloudflare rebuilt Next.js on Vite, and whether this experimental framework is worth watching.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now