Redux introduces a lot of complexity to our codebase with the excessive amount of code it requires. This makes it an imperfect solution for state management in React applications.
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.
The React Context API
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(threading):
<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.
The useReducer
Hook
he useReducer
Hook came with React 16.8. Just like the reduce() method in JavaScript, the useReducer
Hook receives two values as its argument — a reducer function and an initial state — 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
let’s see what happens when we combine useReducer
with Context API
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 is 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.
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
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.
Top comments (0)