Learning redux? Or useReducer
? Then chances are you've been frustrated by the black magic that is the dispatch
function 🧙♂️! Well, luckily you have found this post. I will help you understand what happens under-the-hood and remove the mystery behind dispatch
.
What makes dispatch so hard?
There are a couple of reasons that learning Redux or the reducer hooks can be confusing.
First - both flows rely on a programming paradigm called "functional programming". Thinking in this way requires you to switch your mental model of how to manage data in your application. For more on functional programming, read the first half of this article - Functional Programming in JavaScript: How and Why. The basic idea here that makes these patterns hard is that data is flowing from function to function, and often changes shape a little (or even a lot 😬) before your state is updated and the component re-renders.
Second - both flows "abstract" some of the logic into different functions. Think about it. If you're using Redux, you call an action creator function and POOF... a reducer is called and state
and an action object
are passed in. Like, what the heck?!? 😡 The reducer hook flow has one less layer of abstraction, but there is still some there that adds to the confusion.
Rebuilding dispatch
I think looking at how the dispatch function is built really helps remove the mystery behind reducers. So let's build a simple implementation of dispatch to see the logic that is abstracted out of our view. We start with the function definition.
function dispatch() {
}
Wooooo 🎉! We're doing great so far 😁. Next step, we will add action
as a parameter for the function.
function dispatch(action) {
}
So, with this, we know that when the dispatch function is called, it will be passed an action object as an argument. If you're using useReducer
or useDispatch
, you already know this. Upon some kind of event occurring in the UI, you, not the Redux library, call the dispatch function like this: dispatch({ type: 'ACTION_TYPE' })
. If you're using Redux and the connect
function, even that part is abstracted away from your view and it’s the Redux library that calls the dispatch function. We’ll talk more on that towards the end. Let's continue though.
Now we need to do a couple of checks. We need to make sure that the action object that is passed in is an object, and that it has a type
property. If either of those is not true, we will throw an error. When a reducer function is written, it assumes that those are both true.
function dispatch(action) {
// check that the action argument is an object
if (typeof action !== 'object' || obj === null) {
throw new Error('actions must be plain object.');
}
// check that the action object has a 'type' property
if (typeof action.type === 'undefined') {
throw new Error('Actions may not have an undefined "type" property.'
}
}
Good. Now we can build our reducers with confidence knowing that any action that is dispatched will be an object and will have a "type" property.
Now the exciting part! The next thing we will do is call the reducer from within the dispatch function. This is the abstraction part that hides from our view of what is happening behind-the-scenes. There are a couple of points that we need to cover before we can write this though.
The dispatch function is in the same scope as the current state of the app. So that means that inside the dispatch function, we have access to an object called currentState
that is the current state in our app.
In that same scope is the reducer function that we have written and passed into createStore
or useReducer
. So the dispatch function also has access to reducer
- our reducer function (no matter what we called it) that we passed in. That means that the dispatch function can invoke the reducer function.
Here’s a very simplified version of what that looks like:
const createStore = () => {
// 😮 yep, it’s createStore! But that’s for another article…
// state will be initialized then stored here
const currentState = {};
// your reducer, or combined reducers, will be accessible here
const reducer = null;
// dispatch function in the same scope will have access to the most current state and your reducer(s)
const dispatch = (action) => {
// … all the codes
}
🤯 I know, I know… really cool to see what it looks like under-the-hood, right? Functions and objects. Welcome to functional programming in JavaScript! Once you see it written out like this, it starts to come together! But there is still just a little more to explore.
Let's think about everything that we've learned so far and combine that new knowledge with what we know about reducers.
-
dispatch
has access tocurrentState
andreducer
. - When
dispatch
is called, it receives an action object as an argument. - A reducer function, when invoked, is passed two arguments -
state
(meaning the current state) andaction
. See where I'm going with this?
Inside dispatch
we will now call reducer
and pass in currentState
and the action
object.
function dispatch(action) {
// check that the action argument is an object
if (typeof action !== 'object' || obj === null) {
throw new Error('actions must be plain object.');
}
// check that the action object has a 'type' property
if (typeof action.type === 'undefined') {
throw new Error('Actions may not have an undefined "type" property.');
}
// call the reducer and pass in currentState and action
// reducer and currentState are within scope, action is the parameter passed into the function
reducer(currentState, action);
}
Look at that closely... when an action is dispatched, or in other words, when we invoke dispatch
and pass in an action object, the dispatch
function calls our reducer and passes in the current state and the action object! 🤩 It's all starting to make sense!
Well, there's one last part to this - updating the state. Think about how you write a reducer function. What does it return? It returns a new state object, right? You've followed immutable principles to return a copy of the old state, updated with new data based on whichever action you had dispatched. So when the dispatch
function does this - reducer(currentState, action);
- that function call is going to return a brand new state object. Our dispatch function here needs to update currentState with the new state object that is returned by calling the reducer.
function dispatch(action) {
// check that the action argument is an object
if (typeof action !== 'object' || obj === null) {
throw new Error('actions must be plain object.');
}
// check that the action object has a 'type' property
if (typeof action.type === 'undefined') {
throw new Error('Actions may not have an undefined "type" property.');
}
// call the reducer and pass in currentState and action
// capture the new state object in currentState, thus updating the state
currentState = reducer(currentState, action);
}
And voila! We have built a simple implementation of the dispatch
function. Now, of course, there is more to this in the actual implementations. In Redux, dispatch
needs to tell the app that the state has been updated. This happens through listeners and subscriptions. In the useReducer
hook, React recognizes that the state was updated and re-renders the component. The updated state is then returned to the component from where the useReducer
hook was called.
Regardless of the extra implementations, building out the dispatch
function here will really help us understand what is happening under-the-hood when we call dispatch
from our components.
Redux and action creators
If you're using Redux and connect
, there is one more layer of abstraction to explore. With the connect
function, you pass action creators into an object in the connect
function. The action creators are then passed to the component via props. In your component, when you call the action creator, it will call dispatch for you. That's the added layer of abstraction. Let's look at what connect
does under-the-hood (again in a simplified version).
// inside the connect function implementation
dispatch(actionCreator());
So, connect
wraps the dispatch
function around the action creator call. When the action creator is invoked, it returns an action. So the above evaluates down to:
dispatch({ type: 'ACTION_TYPE' });
which we now understand will call the reducer! Wooo! 🚀
Conclusion
Hopefully, this helps remove the black magic of reducers and dispatch! If you think through the logic flow, you will realize that this is all about functions calling functions and passing data around. And now that the black magic of Redux has been removed a bit, you can get back to the fun part of building web apps with React and Redux ⚛️!
Top comments (18)
The disptach function calls a reducer function, but i came to know that the control flows in this way.
dispatch -> store -> combineReducer -> reducer
I am not understanding that disptach function is calling the exact reducer function but why the the data is passed to store first then to combineReducer and then to reducer.\
Please can you explain this
Hello Habib,
This is absolutely right! Sorry I was late on the reply. Hopefully kelvin's response helped you understand how this system works with combined reducers. Feel free to reach out if you have any other questions! And thank you Kelvin. That was a great explanation 👍👍
Thank you very much for understanding.
Thank you Dustin, I've been spinning my wheels for more than a week to understand useReducer and at last it makes sense!
Seriously the best kind of reply! Thank you, and I'm glad it was helpful!
Thanks a lot! It's pretty clear and helpful
Thank you! I'm really glad this helped!
If you have multiple reducers, can you tell me how the dispatch function knows which reducer to call?
Is there a switch statement internally with action type as the parameter?
Update: Looking at the source code for createStore it looks like it only ever takes a single reducer? Does that mean you simply use combineReducer() and createStore will choose the correct reducer to use?
Actually when you dispatch an action, it will go through all your reducers that you've "combined". It's the action type that's the magic here. You probably only have one reducer that handles that specific action type. All the other reducers will look at the action type, and return their slice of the state untouched.
Thank you very much. It helped very much. Before reading this I was in confusion but you clear all of my doubts once again thank you very much.
Awesome!! So glad it helped 😊
I came here trying to understand why the actionCreator function can't call the dispatch directly ... why do we have to do
dispatch(actionCreator());
instead of justactionCreator();
I tried, but I could not get away from Redux. So, thanks for this amazingly simple refresher! :)
Thanks Leslie! 😁
it helped :)
Yay!! That makes me so happy!
Thank you. I'm brand new to React and this is helping me grok dispatch and reducers.