Written by Ebenezer Don✏️
Redux introduces a lot of complexity to our codebase with the excessive amount of code it requires. At best, this makes it an imperfect solution for state management in React applications. And yet, far too many React developers default to Redux for state management without considering other alternatives.
In this article, I will introduce the React Context API for state management, and show you what makes React Hooks plus the Context API a better solution than Redux.
Why we need a state management tool
In typical React, the way to handle data between disconnected components is through prop drilling. Since there is no global state that components can access if, for instance, you want to pass data from a top-level component to 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. In order to solve this problem, we needed a way to provide a global state that all components, no matter how deeply nested they are, could access.
By solving this, Redux, an open-source JavaScript library for managing application state, became the go-to solution for React developers.
How Redux works
The Redux documentation describes it as a predictable state container for JavaScript applications that helps us to write applications that behave consistently, run in different environments, and are easy to test.
One disadvantage of prop drilling is the need for writing a considerable amount of extra code in order to access data from a top-level component. With Redux, this disadvantage is felt even more as a lot of complexity comes with all its extra code required for setting up a global state for our application. Redux requires three main building parts to function: actions, reducers, and store.
Actions
These 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;
The type
is usually in all caps, with its words separated by underscores. For example, SIGNUP_USER
or DELETE_USER_DATA
.
Reducers
These are pure functions that implement the action 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;
Store
The store is where the application’s state is housed. There is only one store in any Redux application:
import { createStore } from 'redux'
const store = createStore(componentName);
Since our application can only have one Redux store, in order to create a single root reducer for all our components, we’ll need the combineReducers
method from Redux.
With this long process and considerable amount of code required to set up Redux, imagine what our codebase will look like when we have multiple components to work with. Even though Redux solves our state management problem, it is really 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 this problem. When combined with React Hooks, we have a state management solution that is less time-consuming to set up, has an easy learning curve, and requires minimal code.
The React Context API
The new Context API came with React 16.3. Here’s how Context is explained in the React documentation:
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
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 React from 'react';
const newContext = React.createContext({ color: 'black' });
The createContext
method returns an object with a Provider
and a Consumer
component:
const { Provider, Consumer } = newContext;
The Provider
component is what 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. This 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 combined with the useReducer
Hook, we have a solution that finally solves the state management problem.
What are Hooks in React?
Hooks are a type of function that enables the execution of custom code in a base code. In React, Hooks are special functions that allow us to “hook into” its core features.
React Hooks provide an alternative to writing class-based components by allowing us to easily handle state management from functional components.
The useContext
Hook
If you noticed, when explaining the React Context API, we needed to wrap our content in a Consumer
component and then pass a function as a child just so we could access (or consume) our state. This introduces unnecessary component nesting and increases the complexity of our code.
The useContext
Hook makes things a lot nicer and straightforward. In order to access our state using it, all we need to do is call it with our created context
as its argument:
const newContext = React.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.
The useReducer
Hook
The useReducer
Hook came with React 16.7.0. Just like the reduce()
method in JavaScript, the useReducer
Hook receives two values as its argument — in this case, the current state and an action — and then returns a new state:
const [state, dispatch] = useReducer((state, action) => {
const { type } = action;
switch(action) {
case 'action description':
const newState = // do something with the action
return newState;
default:
throw new Error()
}
}, []);
In the above block, we’ve defined our state and a corresponding method, dispatch
, handling 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 (
<button onClick={() =>
dispatch({ type: 'action type'})}>
</button>
)
The useReducer
Hook plus the Context API
Setting up our store
Now that we know how the Context API and the useReducer
Hook work individually, let’s see what happens when we combine them in order to get the ideal global state management solution for our application. We’ll create our global state in a store.js
file:
// store.js
import React, {createContext, useReducer} from 'react';
const initialState = {};
const store = createContext(initialState);
const { Provider } = store;
const StateProvider = ( { children } ) => {
const [state, dispatch] = useReducer((state, action) => {
switch(action.type) {
case 'action description':
const newState = // do something with the action
return newState;
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
that we explained earlier to create a new context. Remember that the createContext()
method returns an object with a Provider
and Consumer
component. This time, we’ll be using only the Provider
component and then 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.
Accessing our state globally
In order to access our state globally, we’ll need to wrap our root <App/>
component in our StoreProvider
before rendering it in our ReactDOM.render()
function:
// root index.js file
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { StateProvider } from './store.js';
const app = (
<StateProvider>
<App />
</StateProvider>
);
ReactDOM.render(app, document.getElementById('root'));
Now, our store context
can be accessed from any component in the component tree. To do this, we’ll import the useContext
Hook from react
and the store
from our ./store.js
file:
// exampleComponent.js
import React, { useContext } from 'react';
import { store } from './store.js';
const ExampleComponent = () => {
const globalState = useContext(store);
console.log(globalState); // this will return { color: red }
};
Adding and removing data from our state
We’ve seen how we can access our global state. In order 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
(the action description as defined in our StateProvider
component) as its parameter:
// exampleComponent.js
import React, { useContext } from 'react';
import { store } from './store.js';
const ExampleComponent = () => {
const globalState = useContext(store);
const { dispatch } = globalState;
dispatch({ type: 'action description' })
};
Conclusion
To a good extent, Redux works for state management in React applications and has a few advantages, but its verbosity makes it really difficult to pick up, and the ton of extra code needed to get it working in our application introduces a lot of unnecessary complexity.
On the other hand, with the useContext
API and React Hooks, there is no need to install external libraries or add a bunch of files and folders in order to get our app working. This makes it a much simpler, more straightforward way to handle global state management in React applications.
Editor's note: Seeing something wrong with this post? You can find the correct version here.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Try it for free.
The post Use Hooks + Context, not React + Redux appeared first on LogRocket Blog.
Top comments (4)
It is very bad solution. Please check performance on medium/large apps - any state update will generate hundreds of children components rerenders(even if nested state variable used by them is not affected). Its much better to implement tiny selector/subscribtion rather than this.
I agree. Also, Context api is best used for components which don't change much. I made a mistake of using context api and hooks. I took a huge performance hit.
I've used both context with hooks and react + redux, it depends on your application.
for simple applications, it is an overkill to use redux on an app that does some API calls hence it doesn't make sense to use redux (you can read Dan Abramov "You might not need redux" )
as for a large scale application, it will become really tedious to do it in context and hooks while it will be less of a hassle to deal with in redux.
That said, it depends on the project along with the team that will handle the codebase so good luck.