loading...

What Even Is A Dispatch Function?

dustinmyers profile image Dustin Myers Updated on ・5 min read

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.

https://images.unsplash.com/photo-1551269901-5c5e14c25df7?ixlib=rb-1.2.1&q=85&fm=jpg&crop=entropy&cs=srgb

What makes dispatch so hard?

There are a couple of reasons that learning Redux or the reducer hook 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 the idea that data is flowing from function to function, and often changes shape a little 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() {

}

Woo! 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 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. 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. 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.

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 to currentState and reducer. 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) and and action. See where I'm going with this?

Inside dispatch we will now call reducer, 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(currentState, action);
}

Look at that closely... when an action is dispatched, or 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 apps.

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 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).

// in connect
dispatch(actionCreator());

So, connect wraps the dispatch function around the action creator call. When the action creator is invoked, it returns an action, right? 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! If you think through the logic flow, you will realize that this is all functions calling functions and passing data around.

Posted on by:

dustinmyers profile

Dustin Myers

@dustinmyers

Interim Web Program Manager and React Instructor at Lambda School. Passionate about programming, learning, and teaching.

Discussion

pic
Editor guide
 

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 tried, but I could not get away from Redux. So, thanks for this amazingly simple refresher! :)

 

Thanks Leslie! 😁

 
 

Yay!! That makes me so happy!