State Management: Redux
Trying to improve my understanding and expertise on
redux I decided to put together a bunch of code sandboxes creating my own version of the library (a simplified one) both in vanilla JS and going to the core of what redux does, but also in react. For the react one I also created a simple version of
Finally, also to understand how redux handles more complex use cases, I also created versions of
redux-thunk and middlewares, to get a better understanding of dealing with side effects in redux. Having gone through that, I thought it would be worth to write a little article describing my understanding of this libraries. Before we get into it, here you have a link to all the different sandboxes so you can check the code out!
When I think about how using redux in a react codebase looks like, I break it down in the following areas:
1. The store
The first step to using redux, and really the core of it, is the store. The store is the object that contains all the elements that make redux redux. To simplify it, we have:
- State: normally when people think of the store, they think of the global state that redux holds for our app. Normally the store object won’t export the state itself but rather a getter function to access it.
- Subscription: if you look at my vanilla js implementation, you’ll see that the next element we’re gonna need the store to provide us is a way for us to susbcribe to store changes, so that when our state changes, we can update other things accordingly. i.e: our ui.
- Dispatcher: finally, we also need the ability to change that state, to update it. Now the whole point of redux, or at least one of it’s biggest wins, is that it makes your system behave a bit like a state machine. You have a define set of actions that you can trigger to modify your state. You don’t just say “update my state to x” like you would with a
const [state, setState] = useState(), potentially introducing bugs. You define all the possible actions you may want to take to modify your state, and then you use a dispatcher to dispatch | trigger those actions.
Optionally, when you create your store, you may also choose to add middlewares to it. Middlewares are functions that will get triggered whenever an action has been dispatched but before it hits the reducer, giving us the ability to run side-effects to actions before they modify state. They can be used for anything from loging, to data fetching. Some popular middlewares help us handle async processes, redux-saga and redux-observable are most commonly known. We’ll talk about them more in depth later.
2. Context Provider | HOC
We’ve create our store, we know what a redux store will normally contain and what the purpose of each method is. Now the question is, how do I let any of my components across my react app tree have access to it?
In the case of my simple vanilla js todo app, the whole app logic is small enough to live in one module and I can initialise the store and use everything I need from it in the same place. If I however needed to access that store in different modules, how would I do that?
I would probably create my store in it’s own module and then import it wherever I need it, right? Now if I wanted to “inject” my store into every single component down the root DOM tree so that they could easily access the store and get state, dispatch actions, etc, how would I do that in react?
Context may be the first thing that comes to mind, I create my store on my
index.js and wrap the
<App /> on a context provider and now all it’s children will be able to consume that context. That is correct, and it is in fact what
react-redux Provider does for us, it is essentially a context provider. How do you now consume that data and stay subscribed to it so you can re-render when it changes? Well now we have some amazing custom-hooks that react-redux provides for that but originally…
Higher order components!
react-redux used to provide us a function known as
connect that we would have to wrap our components with to give them access to the store. How would that work?
What we are we doing here? What does that connect function do? It seems that it takes a function as a paramter and then, over whatever that returns (hint: it returns another fn) it calls that returned thing passing the component as a param.
connect works is, you give it a
mapStateToProps function (a function that will take the store’s state as a param and return and object so that such object can be spread into your component as props based on the state. aka: maps the state to your intended props 😜) and it will return a function.
You then call that function it returns and pass it your component, so that it can spread the state as props into it, together with the dispatch method. This function doesn’t only pass those props, it also wraps the component you are passing to it in a custom component that will be subscribed to any state updates. Finally it wraps the result of that in another component that wraps it in a context consumer. All of that, that we would need to do to each of our components who use the store, it’s magically done by
connect. If you want to see how it does that, you can see it in this sandbox.
Ok that’s all good and fine but we don’t really ever write class components and HOCs anymore, what does this look like in modern react, with custom hooks and all that jazz? 🎷
3. Select and Dispatch
True that, this is not a pattern we would use a modern codebase, nowadays, hooks can solve a lot of our problems and give us a much more ‘reactlike’ way of doing things. And so, having created a store, wrapped my app with a context provider that has that store as value, how would I today access the state and dispatch some actions within a child component?
You would use the
useSelector hooks that
react-redux comes with. How? It would look something like this:
That’s it? That’s it. Surely it cannot be that simple! It is. You may want to define your selectors and action creators in some place and just import them, rather than define them in the component, but that’s about it. How is this possible? Well, these hooks are doing a lot of the heavy lifting for us behind the scenes, let’s take a look a simple implementation:
As you can see, in the case of dispatch, we just need to extract it from the context we have available, not too hard but nice to have that out of a hook and not have to import the context, etc. Also, it is important to point out that dispatch as a method exported by the store, already contains the ability to trigger updates to all existing subscriptions.
On the selector side, we get our getter fn out of the store, use it to get the store’s state, we create a local state to keep track of it’s changes and we subscribe to the store passing a callback that will update our local state when the external one changes. We then select a particular slice of that state by running the callback passed into the hook (aka: the selector) and return that slice of state the user selected.
You can have a look at what a full simple implementation of
react-redux in a modern codebase would look like here.
Also, even though we don’t see any reference to it in these hooks, remember that when you use that dispatch function to dispatch an event, the store itself will make sure to run any middleware you initialise it with before sending such action into the reducer.
4. Actions (and constants)
So we’ve talked a lot about the whole redux flow and how actions get dispatched. But what is an action? We said that one of the great things of redux is the fact that rather than just letting update the entire state however we want anywhere at any time (although it is technically possible if you wanted to) redux forces us to define specific actions that will update our state in especific ways.
Let’s say we want to let the user change it’s username and when they press ‘save’ that will change the app’s state to keep the new username. If we just had a massive object as our state and we needed to update it from the component that lets the user change their username, we would have to pull the entire thing, modify just that one bit - which if you make any mistake will cause a bug in your entire state, potentially affecting other areas of the app with important data - and update it all.
All of that, when what we really wanna do here is UPDATE_USERNAME, so why not do that? Why not create an action that does JUST THAT. If we did that, we could think about it in the following way. We need to know what the action is meant to do, what TYPE of action it is, and what the new value that we want update our piece of state with is, some sort of PAYLOAD. And so, this particular action could look something like…
That’s it. That’s our action, and really, that’s the notion around any action. There’s two things we could comment on here. You may want to give that
payload another name depending on the action or may want to always keep it as
payload, some people define their actions like:
That’s absolutely fine, you can call the payload what you want, you’ll just have to get that data out of the action later on in the reducer (we’ll talk about that in a second).
The other comment I would make is, you may need to reference that action type somewhere else (i.e: in the reducer) so it might be worth keeping a module full of constants for each of your action types so that if you need to change them, you can change them once and update everywhere.
We’ve just mentioned reducers in the last chapter and we also said that our middlewares run after the action has been dispatched but before it reaches the reducer… So what are these reducers we keep mentioning?
A reducer is basically a switch statement. A function, that takes the store’s state and an action as parameters and returns a new version of the state. It is the machine in charge of updating our state based on the actions we send to it. Let me show you visually how that looks like so you get a better idea of what I’m talking about:
Now that function is being passed initially as a param to the
createStore function that redux gives us, so that the store knows to use it accordingly and whenever an action gets dispatched, call this reducer function passing the latest state and the dispatched action to it, and setting the store’s state to whatever that function returns. (See why having the action types as constants could be useful? 😜)
Final point to make here is inmutability, this is one of the most important areas of your app, it defines how your state is updated so you want to make sure there are no bugs here. One good way of accomplishing that is to use a library like Immer or Inmutable.
Now that we have covered the entire flow of using a simple redux setup, what if we wanted to do something more… complex? What if we want to log every time an action is dispatched? What if we need to do some async data fetching when an action is dispatched? Here’s where middleware comes to play, but before that I want you to hear about thunks.
What are thunks and what do they have to do with middlware? We’ve defined our actions as objects right? They have a type and a payload, they get dispatched by a dispatch function and then redux is smart enough to pass them into the reducer and update our state.
What if we made it so that our actions, didn’t only do that, but also let us run some sort of side effect, on a per action basis? What do I mean by that? Let’s say that as part of my update username action, I want to log out the username. How could I do that?
What if instead of dispatching the action itself onClick, I had a function, a function that is going to run whatever side effect I want to run, and then dispatch the action to the reducer? Maybe something like this:
Here we have a function called
handleUpdateUsername that I can call on my onClick, it takes the new username, it seems to return a function that willl somehow get
dispatch injected into it. This function makes some API call to update the username, probably on the database - so we can persist the change outside of our store - then, if the API call went well, it uses that injected dispatch to dispatch an action (created by the action creator defined above) and if something goes wrong it creates an error alert for the user.
Wow, that was a lot more that just updating the state, wasn’t it? This is more like the kind of stuff that you may need to do in your app. You don’t only update state but you run some side effects with the update. Anything from optimistic updates to logging… endless posibilities!
How would you use this new function or “thunk” we have created though? Simple, you would dispatch it just like any other action, something like this:
const handleOnClick = () => dispatch(handleUpdateUsername(userName));
Hold on a second, our
handleUpdateUsername is somehow being passed the
dispatch method as a param so it can dispatch it’s own action, where is that happening? Good eye if you caught that, we are not passing here, we didn’t have to do something like this:
const handleOnClick = () => dispatch(handleUpdateUsername(userName))(dispatch);
So where are we doing it? Well, we aren’t, our
redux-thunk middleware would inject it for us if we passed it into the
createStore function when we defined it. So we are finally gonna talk about middleware? Yes!
When we create our store, one of the params we can pass into it, is a set of middlewares, that would look something like this: