So when was the last time you tried managing application state, without Redux? As far as I can remember, for me, it was probably during the time I had started learning and playing around with React. Although I had known about Redux at the time when I did try not using it, I just wanted to do it because I didn’t want to add three dependencies to my react application just for managing a tiny application state. Of course, when we talk about using redux, we are also going to use react-redux
and middleware too!
I did it in two different ways ( which most of us, might have tried at some point as well )
Using localStorage
and custom listeners.
Using the Context API.
But in this article, we’re not going to discuss that. We’re going to see another way of managing the state, which is relatively new — using Hooks.
So, let’s set up a react project and add a root component, like so :
import React from "react";
import "./App.scss";
import ChildComponent from "./ChildComponent";
const App = () => {
return (
<>
<div className="app">
<h2>Welcome to React!</h2>
</div>
<ChildComponent />
</>
);
};
export default App;
And our ChildComponent.js
as,
import React from 'react';
const ChildComponent = () => {
return (
<div>
<p>This is the child component</p>
</div>
);
}
export default ChildComponent;
First, let us break down the complete flow and decide what we need :
A state, of course
Update our state.
Sync the data between the state and our components wherever required.
Do all of this while keeping the code clean. ( Very important )
Let’s set up our state first. For this, I’m going to use the useReducer
hook. For those who are not familiar with the useReducer
hook — it is similar to the basic useState
hook but more suited for this case as it provides a dispatch
method, the previous state while computing and updating the state. It provides us a way that is similar to Redux’s reducer and action flow. Let’s set up our useGlobalState
hook, which helps us initialize our state and provide us a dispatch for updating it as well.
So our redux.js
looks like this :
import React from "react";
export const useGlobalState = (initialState = {}, reducer) => {
let init;
let appState = {};
if (typeof initialState === "function") {
init = initialState;
} else {
appState = initialState;
}
const [state, dispatch] = React.useReducer(
reducer,
appState,
init
);
return {
...state, dispatch
};
};
So what’s going on here? Our custom hook here takes two arguments — initialState
for setting an initial state to our app and reducer
is our reducer function for updating state depending on the actions.
Our reducer.js
might look like this :
export default (state, action) => {
switch (action.type) {
case 'increment':
return {
...state,
count: state.count + 1,
};
default:
return state;
}
}
With those two arguments, we can initialize our reducer and state as :
const [state, dispatch] = React.useReducer(reducer, initialState);
Since our initialState
might, in some cases, require some computation and may not be just a static value — we are going to use the third argument of useReducer
to initialize our state from a function just in case we need to. So now we can initialize our state in two ways :
const globalState = useGlobalState(intialState, reducer);
// OR
const globalState = useGlobalState(() => {
// Do stuff here.
return state;
}, reducer);
But where do we initialize our state? Let’s add this to our App.js
file and use the classic example of making a simple counter.
import React from "react";
import { useGlobalState } from "./redux";
import reducer from "./reducer";
import ChildComponent from "./ChildComponent";
import "./App.scss";
const App = () => {
const globalState = useGlobalState({ count: 0 }, reducer);
return (
<>
<div className="app">
<h2>Welcome to React!</h2>
<h2>{globalState.count}</h2>
<button type="button" onClick={() => globalState.dispatch({ type: "increment" })}>
Click me to plus one!
</button>
</div>
<ChildComponent />
</>
);
};
export default App;
This gives us something like this :
But still, we can’t use the state inside our ChildComponent
since it has no idea of this state. We’re going to use the createContext
API for that.
Let’s update our redux.js
to give us a way to pass the state to our child(ren) component(s), like so : :
import React from "react";
export const context = React.createContext();
export const useGlobalState = (initialState = {}, reducer) => {
let init;
let appState = {};
if (typeof initialState === "function") {
init = initialState;
} else {
appState = initialState;
}
const [state, dispatch] = React.useReducer(
reducer,
appState,
init
);
return { ...state, dispatch };
};
export const Provider = ({ children, initialValue }) => {
return (
<context.Provider value={{ ...initialValue }}>
{children}
</context.Provider>
);
};
I guess you can see where this is going. We are going to use the Provider component in our root App.js
and wrap our component with it.
Additionally, we’ll pass an initial state as the value for our ‘provider.’ This makes the values available in the DOM tree. But then you might wonder — we need to wrap all our children who are going to use the state with context.Consumer
, don’t we? Well, no.
Here’s were our React.useContext
hook comes into play along with a little HOC trick. And we’re going to name it connect
, so it looks similar to redux! Also, it can be easier to understand if we can visualize it in the ‘redux way.’ But first, let’s check if our current setup works.
Update the App.js
file to this :
import React from "react";
import { useGlobalState, Provider } from "./redux";
import reducer from "./reducer";
import ChildComponent from "./ChildComponent";
import "./App.scss";
const App = () => {
const globalState = useGlobalState({ count: 0 }, reducer);
return (
<Provider initialValue={globalState}>
<div className="app">
<h2>Welcome to React!</h2>
<h2>{globalState.count}</h2>
<button type="button" onClick={() => globalState.dispatch({ type: "increment" })}>
Click me to plus one!
</button>
</div>
<ChildComponent />
</Provider>
);
};
export default App;
And our ChildComponent.js
like this :
import React from 'react'
import { context } from './redux';
const ChildComponent = () => {
const state = React.useContext(context);
return (
<div>
<p>This is the child component.</p>
<p>{state.count}</p>
</div>
);
}
export default ChildComponent;
So what does useContext
hook do? Well, it’s similar to using context.Consumer
tag which allowed us to access context value and subscribe to its changes. With useContext
hook, we no longer use the context.Consumer
in our component. We pass the context object to it, which then returns the value from the current context. Whenever the context data changes, the component is re-rendered with the new values.
Let’s see if this works.
Great! But there’s one thing. Now we need to call useContext
in every component! Let’s get rid of this. We’re going to write a small HOC which exposes an API similar to the connect
HOC from react-redux
.
Now, our redux.js
should look like this :
import React from "react";
const context = React.createContext();
export const useGlobalState = (initialState = {}, reducer) => {
let init;
let appState = {};
if (typeof initialState === "function") {
init = initialState;
} else {
appState = initialState;
}
const [state, dispatch] = React.useReducer(
reducer,
appState,
init
);
return { ...state, dispatch };
};
export const connect = (mapState, mapDispatch) => {
const componentWithNewProps = (ReactComponent) => {
const componentWithContext = (thisprops) => {
const { dispatch, ...rest } = React.useContext(context);
const mappedProps = mapState ? mapState(rest) : {};
const mappedDispatch = mapDispatch ? mapDispatch(dispatch) : {};
const props = {
...mappedProps,
...mappedDispatch,
};
return <ReactComponent {...props} {...thisprops} />
}
return componentWithContext
}
return componentWithNewProps
}
export const Provider = ({ children, initialValue }) => {
return (
<context.Provider value={{ ...initialValue }}>
{children}
</context.Provider>
);
};
As you can see, we are just spreading the props over the component here. The ideal way is to use the hoist-non-react-statics
package to copy all non-react static methods to the new component. Please use that way since it is better than just passing the props. A complete explanation is in the React Documentation for Higher-Order Components.
The connect
HOC here takes our component and uses the context to get all the props that are required by the component as defined in the mapStateToProps
function in the connect
call. We can update our ChildComponent.js
now, to something like this :
import React from 'react';
import { connect } from './redux';
const ChildComponent = ({ count, updateCounter }) => {
return (
<div>
<p>This is the child component</p>
<p>{count}</p>
<button type='button' onClick={updateCounter}>Click me, I can plus one too!</button>
</div>
);
}
const mapState = ({ count }) => ({
count
});
const mapDispatch = (dispatch) => ({
updateCounter: () => dispatch({ type: 'increment' })
});
export default connect(mapState, mapDispatch)(ChildComponent);
Let’s check if this works.
In case you’re wondering, you can have different counters for both of them, and it’ll work just fine! You need to initialize the state with both the counters, dispatch actions from their respective buttons, and use the respective values from the state to display. Like so :
// In App.js, initialize counters like this
const globalState = useGlobalState({ count: 0, anothercount: 1 }, reducer);
/**
* In ChildComponent.js, update the `mapState` and `mapDispatch` methods
* to get and update `anothercount` value from state.
*/
const mapState = ({ anothercount }) => ({ // Get the `anothercount` value from state.
count: anothercount,
});
const mapDispatch = (dispatch) => ({
// Update the dispatch to trigger `countincrement` action.
updateCounter: () => dispatch({ type: 'countincrement' })
});
export default connect(mapState, mapDispatch)(ChildComponent);
/**
* Finally, update our reducer to handle `countincrement` action,
* which updates the `anothercount` value in our state.
*/
export default (state, action) => {
switch (action.type) {
case 'increment':
return {
...state,
count: state.count + 1,
};
case 'countincrement':
return {
...state,
anothercount: state.anothercount + 1,
};
default:
return state;
}
}
Oh, and one more thing! Don’t forget to wrap your components with React.memo
if they’re not connected to the state. This prevents unnecessary re-renders when the state updates.
And we’re done implementing a small redux-like application state management in our react application! All within just 40 lines of code! ✨
You can check out the complete example in this Github repository. Please leave a star on the repository or comment here if you liked this article!
If you have any suggestions or ideas, you can also contact me through my social media profiles.
Thank you for reading! 😄
Happy hacking! Cheers! 🎉
May 11, 2019
Learn how to architect complex React applications with Redux, Redux-Saga, and service layers. This guide provides a scalable structure for organizing reducers, actions, middlewares, and selectors in large React projects.
April 11, 2019
Learn how to structure your React applications with a scalable and maintainable architecture. Discover practical directory organization patterns based on real-world experience.
September 21, 2018
Learn how to implement secure, encrypted data storage in React Native applications using redux-persist and redux-persist-transform-encrypt to protect sensitive user information.
June 29, 2019
Learn how to automate your Lighthouse audits with Mocha and Chai instead of manually performing audits on your Progressive Web Application. Run tests programmatically in CI/CD environments or locally to maintain consistent quality standards.
February 14, 2024
Learn how to use JSON-LD structured data to enhance your website's SEO, improve rich snippets, and help search engines better understand your content.
November 19, 2023
Learn how to implement Microdata and Microformats to improve your website's SEO, help search engines understand your content, and increase visibility in search results.