Disclaimer: This solution is best suited for small scale projects and its main motive is to explore the new APIs react provides rather than trying to replace any traditional solutions.
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 really small application state. Ofcourse, when we talk about using redux, we are also going to use react-redux
and a middleware too!
I basically 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 fairly new - using Hooks.
So, let's setup a react project and add a root component, like so :
And our ChildComponent.js
as,
First, let us break down the complete flow and decide what we need :
A state, ofcourse
A way to mutate / update our state.
A way to sync the data from state to our components wherever required.
Do all of this while keeping the code clean. ( Very important )
Let's setup 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 state, etc. Basically, it will provide us a way which is similar to Redux's reducer and action flow. Let's set up our useGlobalState
hook which will help us initialize our state and provide us a dispatch for updating it as well.
So our redux.js
looks like this :
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 which is going to be used for updating state depending on the actions.
Our reducer.js
might look like this :
With those two arguments, we can initialise 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 initialise our state from a function just incase we need to. So now we can initialise our state in two ways :
const globalState = useGlobalState(intialState, reducer);
// OR
const globalState = useGlobalState(() => {
// Do stuff here.
return state;
}, reducer);
But, where do we initialise our state? Well, this needs to be done inside the root component since Hooks can only be called / used inside a functional component. Let's add this to our App.js
file and use the classic example of making a simple counter.
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. So how are we going to do this?
This is a slightly interesting step now. 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 :
I guess you can clearly 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 where 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 will be easier to understand if we can visualise it in the 'redux way'. But first, let's check if our current setup works.
Update the App.js
file to this :
And our ChildComponent.js
like this :
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 :
Note : As you can see, we are just spreading the props over the component here. The ideal way is to use 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. Complete explanation can be found 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 :
Let's check if this works.
Incase you're wondering, you can have different counters for both of them and it'll work just fine! You just need to initialise 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, initialise 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 state. This will prevent unnecessary re-renders when the state udpates!
And we're done implementing a small redux-like application state management in our react application! All within just 40 lines on 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!
You can connect with me via Twitter.
Thank you for reading! 😄
Happy hacking! Cheers! 🎉
Top comments (0)