Amongst the dev community, one of the most hot and trending technology these days has got to be React and regardless of how you feel about working with it, you have to give it credit for at-least feeling like a full-blown framework despite being a library.
Redux has been the library of choice for global state management for years now but it's high time now that we move forward. The primary reason for this being the verbosity of Redux that not just make it a tad cumbersome to use but also kind of senseless unless you are working on an enterprise-level project.
CONTEXT API
React 16 gave us a wonderful solution to global state management => Context API. A library with syntax very similar to Redux but with exponentially less boilerplate and setup.
Using Context is pretty straightforward. A Provider
serves the values:
import React from 'react';
const StateContext = React.createContext(/* default state */);
const App = props => (
<StateContext.Provider value={{ color: green }}>
{props.children}
</StateContext.Provider>
);
And a Consumer
uses the values:
const ConsumerComponent = () => (
<StateContext.Consumer>
{value => (
<Button primaryColor={{ value.color }}>Consumer</Button>
)}
</StateContext.Consumer>
);
This implementation shall, however, work only for functional components
. There are other ways to access context values in class-based components. You may read up on those in the official React docs
...
HOOKS
Soon after the Context API was released in production, hooks
were introduced and things were never the same again. It was not just state management that benefited but React in its entirety saw a much-needed overhaul from all the classes that we used to write previously.
Out of the 10 default hooks that React ships with, the useReducer
hook can be used to set up Context API within our app. The useReducer hook takes 2 arguments; namely a reducer function
and an initial state
:
import React, { useReducer } from 'React';
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
default:
return state;
}
};
const IncrementComponent = ({ initialCount }) => {
const [state, dispatch] = useReducer(reducer, { count: 0 }); return (
<button onClick={() => dispatch({ type: 'increment'})}>
Increment: {state.count}
</button>
);
} ;
This syntax looks awfully similar to Redux. Cool right!
The useReducer hook returns the [state, dispatch]
array where state
is the current state you can consider dispatch
to be like a gun that shoots actions at our data layer. You can call it anything but while looking at a Redux substitute, we can use its naming convention at the very least!
Every time the dispatch
method is called, the reducer function is triggered which matches your action type and updates the state as per the payload you send to it.
Now for the fun part!
HOOKS + CONTEXT
Using Context and hooks together allows you to make your own global state management system in hardly 10 lines of code. Intrigued right because that would be pretty neat!
import React , { createContext , useContext , useReducer } from 'react';
export const StateContext = createContext();
export const StateProvider=({ reducer , initialState , children })=>(
<StateContext.Provider value = {useReducer(reducer,initialState)}>
{children}
</StateContext.Provider>
);
export const useStateValue = () => useContext(StateContext)
Et voila. We're done! Let's see what's happening here:
-
createContext
creates a context object and returns it to StateContext which stores theProvider
and theConsumer
- Next we create StateProvider which is basically a HOC which wraps its children with the values offered by the Provider.
- Finally we create
useStateValue
which is acustom hook
which uses the StateContextcontext
. We did this because otherwise you would have to importuseContext
andStateContext
in all places wherever you wanted access to these values. Building up on the DRY principle of coding, we created a universal hook to take care of the same once and for all.
...
Next we just need a reducer and an initial state to start using the StateProvider
component. We already took a look at what a reducer
could look like earlier. The initial state
can be anything you want; a JSON object, an array, or whatever fits your purposes the best.
Now let's see how to use the StateProvider
component and we're all set. In our entry file (generally App.js), we just need to wrap our return block with the StateProvider =>
import { StateProvider } from './utils/StateProvider';
import reducer, { initialState } from './utils/reducer';
const App = () => {
return (
<StateProvider initialState={initialState} reducer={reducer}>
// Content ...
</StateProvider>
);
}
SIDE NOTE
- To now access the state values inside your component, you simply go like:
import { useStateValue } from '../utils/StateProvider'
const MyComponent = () => {
const [state, dispatch] = useStateValue()
}
You can always use object destructing to just get the required slices of data from your global state instead of getting the entire state.
- This implementation is viable only for functional components because
useStateValue
under the hood use theuseContext
hook and the rules of hooks clearly state thathooks can only be called inside the body of a functional component
. If you want to use these values in aclass-based component
, you shall have to go over the bit more verbose method of using Consumer orcontextType
but in modern times, working with functional components is the norm and there's no way that this implementation shall break any time in the future.
Top comments (3)
i love the explanation sir, btw im using context and reducer for my state management. would u mind to make more article about react ecosystem??
for example preact for nextjs, etc :)
Hey @fandyajpo
So glad that you loved the explanation.
And please feel free to drop down in the comments what else you would like to know and I'll give it my best to simplify it for you as well as I can.
Nice article @anukr98