For anyone who wants to skip the article and see the end result, I’ve taken what I’ve written here and made a library out if it using hooks: use-simple-state. It has zero dependencies (other than react as a peer dependency) and at just 3kb, it’s pretty lightweight.
In recent years the scope of web applications has increased dramatically and as the requirements for our apps grow, so does the complexity. In order to make this added complexity easier to deal with, certain techniques and patterns are increasingly being used to make developer’s lives easier and to help us build more robust applications.
One of the main areas where complexity has grown is in managing our application’s state, so to combat this developers are using libraries that provide abstractions for updating and accessing their app’s state. The most notable example being Redux, which is an implementation of the Flux pattern.
Once a developer has learned how to use a library like Redux, they may still be left wondering just how exactly everything is working “under the hood” since it’s not obvious at first, even if the more general concept of updating a globally-available object is easy to grasp.
In this article we’ll build our own state management solution for a React application, completely from scratch. We’ll start with a basic solution that can be implemented in just a few lines of code and gradually work in more advanced features until we have something resembling Redux.
Any state management tool needs only a couple of things: a global state value available to the entire application, as well as the ability to read and update it. That’s it, seriously.
Just to show you how simple a state manager can be, here’s a barebones vanilla JavaScript implementation:
The above example is as basic as it gets, yet it still ticks all the boxes:
state
getState
setState
The above example is too simple for most real-world applications so next we’re going to start implementing a workable solution for use in a React app. We’ll start by refactoring the previous example to make it work in React and build on from there.
In order to make a React-based version of our previous solution we’re going to need to leverage two React features. The first feature being plain old class components, a.k.a stateful components.
The second feature is the context API, which is used to make data available to your entire React application. A context has two parts: a provider and a consumer. The provider, as the name suggests, provides the context (data) to an application. While a consumer is used when we want to access a context.
A good way to understand context is this: if props are used to explicitly pass data through your components, then context is used to implicitly pass data.
Now we know the tools we want to use, it’s just a case of putting them together. All we’re going to do is create a context to hold our global state, then wrap that context’s provider in a stateful component and use that to manage the state.
First off, let’s create our context using React.createContext
, this gives us our Provider
and Consumer
:
Next, we need to wrap our Provider
in a stateful component in order to leverage it to manage our app’s state. We also want to export the consumer with a more specific name:
In the above code sample, our StateProvider
is simply a component that accepts a state
prop as the initial state and makes whatever is contained in that prop available to any component underneath it in the component tree. If no state
is provided then an empty object is used instead.
Using our StateProvider
is as simple as wrapping it around our application’s root component:
Now we’ve done that, we can access our state from anywhere inside MyApp
using a consumer. In this case we’ve also initialised our state to be an object with a single property: count
, so whenever we access our state now, that is what we will find.
Consumers use render props to pass the context data, this can be seen below where a function is a child of StateConsumer
. The state
parameter passed to that function represents our application’s current state, so as per our initialState
, state.count
will be equal to 0
.
An important thing to note about our StateConsumer
is that it automatically subscribes to changes in the context, so when our state changes the component will re-render in order to display the updates. This is just the default behaviour for consumers, we haven’t done anything to enable it.
So far we’ve built something that allows us to read our state, as well as automatically update when it changes. Now we need a way to update the state of our app, to do this we’re simply going to update the state in our StateProvider
.
As you may have noticed earlier, we passed a prop called state
to our StateProvider
, which was then passed to the component’s state
property. This is what we will be updating, using React’s built-in this.setState
method:
Continuing the theme of keeping it simple, we’ve just passed this.setState
to our context. This meant we had to change the value of our context slightly; instead of only passing this.state
we’re now passing an object with two properties: state
and setState
.
Whenever we use our StateConsumer
we’ll use a destructuring assignment to get state
and setState
, so now we can read from and write to our state object:
Something to note is that since we’ve simply passed React’s built-in this.setState
method as our setState
function, additional properties will be merged with the existing state. This means that if we had a second property in addition to count
then it would be preserved automatically.
Now we’ve built something that could work in the real world (albeit not very efficiently). It’s got a simple API that should feel familiar to React developers, plus it leverages built-in tools so we haven’t added any new dependencies either. If state management libraries felt a bit ‘magical’ before, hopefully we’ve already been able to shed some light on what the internals of one might look like.
Those of you already familiar with Redux may have noticed that our solution is lacking in a few areas:
setState
function and we’re relying on React’s default this.setState
behaviour to handle our state update logic, there’s also no built-in way of reusing state updates, something you get from Redux reducers.To overcome this, we’re going to emulate Redux by implementing our own actions, reducers, and middleware. We’ll also add built-in support for async actions. After that, we’re going to implement a way for our consumers to only listen for changes in a subset of our state. Finally we’ll also look at how we can refactor our code so we’re using the brand new Hooks API.
Disclaimer: the following is only meant to give you enough of an understanding to continue with the article, I’d highly recommend reading the official introduction to Redux for a more thorough explanation.
If you already have a good understanding of Redux, feel free to skip this bit.
Below is a simplified diagram of the data flow in a Redux application:
Redux data flow
As you can see, there is a one way data flow — we dispatch an action from which our reducers derive an updated state, no data is traveling back and forth between different parts of our application.
In a bit more detail:
First, we dispatch an action which describes a change to our state, e.g. dispatch({ type: INCREMENT_BY_ONE })
to increase a number by 1. Contrast this to our previous, more imperative method whereby we essentially manipulated the count
state directly: setState({ count: count + 1 })
.
The action then passes through our middleware. Redux middlewares are optional functions that can perform side effects as a result of actions, e.g. if a SIGN_OUT
action is dispatched you may use a middleware function to remove all user data from local storage before passing the action along to your reducer. If you’re familiar with middleware in Express, this is a very similar concept.
Finally, our actions arrive at our reducers which take the action, as well as any accompanying data, and use that plus the existing state to derive a new state. Let’s say we dispatch an action called ADD
and we also send an accompanying value (called a payload) which is the amount we wish to add to our state. Our reducer will check for an ADD
action, when it finds one it will take the payload as well as the current value in our state and add the two together to produce our updated state.
The function signature for a reducer is as follows:
(state, action) => nextState
A reducer should simply be a function of state
and action.
The API is simple yet powerful. A key thing to note is that reducers should always be pure functions, so that they are always deterministic.
Now that we’ve briefly gone over some of the key parts of a Redux app, we need to modify our app to emulate the same behaviour. First things first: we need some actions and a way to dispatch them.
For our actions we’re going to use action creators, these are simply functions that create actions. Action creators make testing, reusing, and passing payloads to our actions much easier. We’re also going to create some action types, these are just string constants, since they’ll be re-used in our reducers we’ll store them in variables:
For now, we’re going to implement a placeholder dispatch
function. Our placeholder will just be an empty function, which we’ll use to replace the setState
function in our context. We’ll come back to this in a moment, since we don’t yet have any reducers to dispatch our actions to.
Now we’ve got actions we just need some reducers to send them to. Thinking back to the reducer function signature, it’s simply a pure function of actions and state:
(state, action) => nextState
Knowing this, all we need to do is pass our component’s state and the dispatched action into our reducers. For the reducers, we simply want an array of functions that adhere to the above signature. We use an array so that we can simply iterate over it using Array.reduce
until we arrive at our new state:
As you can see, all we do to get our new state is to compute it using our reducers, then just like before we simply call this.setState
to update StateProvider
‘s component state.
Now we just need an actual reducer:
Our reducer just checks the incoming action.type
and if it finds a match it’ll update the state accordingly, otherwise we just fall through the switch
statement and return an undefined
value from our function by default. An important difference between Redux’s reducers and our own is that when we don’t want to update the state, usually because we didn’t find a matching action type, we return a falsy value, whereas with Redux you would return the unchanged state.
And pass our reducer to our StateProvider
:
Now we can finally dispatch some actions and watch our state update according to which ones we send:
Now we’ve got something that resembles Redux a fair bit, we just need a way to handle side effects. To achieve this we’re going to allow our user to pass middleware functions that will be called whenever an action is dispatched.
We also want our middleware functions to be able to bail us out of state updates, so if null
is returned from one we won’t pass the action to our reducer. Redux handles this a little differently — in Redux middleware you need to manually pass the action along to the next middleware, if it is not passed along using Redux’s next
function the action will not reach the reducer and the state will not update.
Now let’s write a simple middleware. We want it to look for an ADD_N
action, if it finds one it should print the sum of the payload
and the existing count
state, but block the actual state update.
Just like our reducers, we’ll pass any middlewares to our StateProvider
in an array:
Finally we need to call all of our middleware and use the result to determine whether or not we want to abort an update. Since we’ve just passed an array and we’re looking for a single value, we’re going to use Array.reduce
to get our result. Just like with our reducers we’ll iterate through the array while calling each function, then pass the result to a variable that we’ll name continueUpdate
.
Since middleware is considered an advanced feature we don’t want it to be mandatory, so if no middleware
prop is found in our StateProvider
we’ll make continueUpdate
equal undefined
by default. We’ll also add a middleware
array as the default prop, just so middleware.reduce
doesn’t throw an error if nothing is passed.
As you can see on line 13, we check to see what our middleware functions return. If a null
value is encountered we will skip the rest of the middleware functions and the value of continueUpdate
will be null
, meaning we will abort the update.
Since we want our state manager to be useful in the real world we’re going to add support for async actions, which will mean we can handle common tasks like network requests with ease. We’re going to borrow from Redux Thunk a bit here since the API is simple, intuitive, and powerful.
All we’re going to do is check to see if an uncalled function was passed to dispatch, if we find one we’ll call it while passing dispatch
and state
which gives the user everything they need to write async actions. Take this authentication action as an example:
In the above example we have an action creator called logIn
, instead of returning an object however, it returns a function that accepts dispatch
. This allows the user to dispatch synchronous actions before and after an asynchronous API call. Depending on the result of the API call a different action will be dispatched, in this case we send an error action if something goes wrong.
Implementing this is as easy as checking action
for a function
type in the _dispatch
method in our StateProvider
:
Two things to note here: where we call action
as a function we pass this.state
so the user can access the existing state inside the async action, we’re also returning the result of the function call, allowing developers to get a return value from their async actions which opens up more possibilities, such as chaining promises from dispatch
.
Something that often gets overlooked yet is an essential feature of Redux (or more accurately, React-Redux — the React binding for Redux) is it’s ability to only re-render a component when necessary. To achieve this it uses the connect
higher order component, which takes a mapping function — mapStateToProps
— and will only trigger a re-render of the component it’s attached to when the output of mapStateToProps
(just mapState
from now on) changes. If this were not the case then every component that uses connect
to subscribe to store changes would be re-rendered every single time the state updates.
Thinking about what we need to do, we’re going to need a way to store previous outputs of mapState
so we can compare it to any new results to decide if we want to go ahead and re-render our component. To do this we’re going to use a process called memoization. Like many things in our industry it’s a big word for a fairly simple process, especially for us since we can leverage React.Component
to store the subset of our state in this.state
and only update it when we detect changes in the output of mapState
.
Next we’re going to need a way to skip unnecessary component updates. React provides an easy way for us to do this by using the lifecycle method shouldComponentUpdate
. It takes any incoming props and state as parameters which allows us to compare the values to our existing props and state, if we return true
the update will go ahead but if we return false
React will skip rendering.
The above is an outline for what we’re going to do next. It has all the main pieces in place: it receives updates from our context, it implements getDerivedStateFromProps
and shouldComponentUpdate
, and it also takes a render prop as a child — just like the default consumer. We also initialise our consumer’s initial state by using the passed mapState
function.
As it is right now though, shouldComponentUpdate
will only render once when it receives the first state update. After that it will log the incoming and existing state and return false
, blocking any updates.
The above solution also calls this.setState
inside shouldComponentUpdate
and as we know this.setState
always triggers a re-render. Since we’re also returning true
from shouldComponentUpdate
, this will cause an additional re-render, so to get around this we’re going to derive our state using the lifecycle getDerivedStateFromProps
, then we’ll use shouldComponentUpdate
to determine whether we want to carry on with the rendering process based on our derived state.
If we inspect our console we can see that the global state updates, while our component blocks any updates to it’s this.state
object and therefore skips rendering:
Three attempts to update state, but thisState only changes once
So now that we know how to prevent an unnecessary update we need a way to intelligently determine when our consumer should render. If we wanted to we could recurse over an incoming state object and check every single property to see if it’s changed, but while this would be a good exercise to improve our understanding it could be bad for performance. We can’t know how deep or complex any incoming state object might be and a recursive function will happily carry on indefinitely if the exit condition is never met, so we’re going to limit the scope of our comparison.
Just like Redux, we’re going to implement a shallow compare function. “Shallow” here refers to the depth of the properties at which we’re going to see if our objects are equal, meaning we’re only going to check 1 level deep. So we’re going to check if each property at the top level of our new state is equal to a property of the same name on our existing state, if properties of the same name don’t exist or they have different values, we’ll proceed with rendering, otherwise we assume our states are the same and we abort the render.
First we start off with a simple check that will look at whether both states are objects, if not then we skip rendering. After this initial check we convert our current state into an array of key/value pairs and check the values of each property against that of our incoming state object by reducing the array into a single boolean.
That’s the hard part out of way. Now that we want to use our shallowCompare
function it’s essentially just a case of calling it and checking the result. If it returns true
we’re going to return true
to allow the re-render, otherwise we simply return false
to skip the update (and our derived state is discarded). We also want to apply our mapDispatch
function, if it exists.
Lastly we need to pass a mapState
function to our consumer that only maps part of our state, so we’ll pass it as a prop to our updatedStateConsumer
:
And now we’re only subscribed to changes in greeting
, so if we update count
our component will ignore the changes in our global state and avoid a re-render.
If you’ve made it this far you’ll have seen how to implement a Redux-like state management library, complete with reducers and actions. We’ve also covered more advanced topics, such as asynchronous actions, middleware, and how to make it so we only receive the state updates we want to avoid re-rendering our consumers each time the global state updates.
While Redux has a lot more going on under the hood than our solution, hopefully this has helped clear up some of the core concepts and shown that while Redux is generally considered to be more of an advanced topic, it’s implementation is relatively simple.
For a more thorough understanding of Redux’s internals, I’d highly recommend reading the source code on Github.
The solution we have so far has all the tools and attributes necessary to be used in a real-world project now. We could start using this in a React project and we wouldn’t need Redux unless we wanted to access some of the really advanced features.
If you haven’t yet heard, hooks are quickly becoming the next big thing in React. Here’s a brief explanation from the official introduction:
Hooks are a new feature proposal that lets you use state and other React features without writing a class.
Hooks give us all the power of higher order components and render props with a cleaner and more intuitive API.
Let’s take a look at how they work using a quick example showing the basic hook useState
:
In the above example we initialise a new state by passing 0
to useState
which returns our state: count
, as well as an updater function: setCount
. If you’ve not seen this before you may wonder how useState
doesn’t get reinitialised to 0
on every render — it’s because React handles this internally, so we don’t need to worry about that.
So let’s forget about middleware and async actions for a moment and re-implement our provider using the useReducer
hook, which works just like useState
, except actions are dispatched to a reducer from which the new state is derived, just like what we’re building.
Knowing this, we simply copy our reducer logic from our old StateProvider
into our new, functional StateProvider
:
That’s how simple it can be, but while we want to keep things simple, we still aren’t fully harnessing the power of hooks just yet. We can also use hooks to swap our StateConsumer
for our own custom hook, which we’ll do by wrapping the useContext
hook:
Whereas before we were destructuring Provider
and Consumer
when we created our context, this time we store it in a single variable which we pass to useContext
in order for us to access our context without a Consumer
. We’ve also named our custom hook useStore
, since useState
is a default hook.
Next we simply refactor the way in which we consume our context:
Hopefully these examples have gone some way in showing how intuitive, simple, and powerful hooks are. We’ve reduced the amount of code needed and given ourselves a nice, simple API to work with.
We also want to get our middleware and built-in support for asynchronous actions working again. To do this we’re going to wrap our useReducer
inside a custom hook, one to be used specially in our StateProvider
, and then simply re-use the previous logic from our old stateful component.
As with our old solution, we want middleware to be optional, so we add an empty array as a default again — although this time we use a default parameter instead of default props. Similar to our old dispatch function, we call our middleware and, if continueUpdate !== null
we carry on with the state update. We’ve also made no changes to how we handle async actions.
Finally, we pass the result of useStateProvider
and it’s parameters to our provider, which has shrunk considerably:
And that’s it! 🎉
One thing you may have noticed is that our hooks implementation has no way to skip unnecessary updates. This is because of how hooks are called in the body of a function component — at that stage React has no way of bailing out of the rendering process (not without some hacks). There’s no need to worry though, the React team are aware of this and plan to provide a way for us to abort an update from functional components.
Once we’ve got an official way to bail out of rendering inside a function component I’ll come back here and update this blog post. In the meantime, the library I’ve written out of the hooks implementation comes with a consumer so we can access this functionality.
To summarise, we’ve taken a look at the most barebones state manager possible and incrementally built upon it until we ended up with something resembling Redux — complete with actions, reducers, middleware, and a way to diff state updates to improve performance. We’ve also looked at how we can simplify our code using the brand new hooks API.
Hopefully you’ve found something useful in this article and I was able to shed a bit of light on some more advanced concepts while showing that a lot of the tools we use may be more simple than they first appear.
As briefly mentioned at the beginning, I’ve written a library, Use Simple State, off the back of this article. You can see it on on my Github page, where I’ve used hooks for the final implementation, which includes a couple of additional features.