How Redux Works - Part 1

This is part one of a two part series. For part two see here.

Create Store

I spend a lot of time walking people through Redux best practices and how to use it, and throughout this process I've found it really helpful to dive into the source itself. While this source is very readable and well commented, I thought I'd put together a blog post to break it down and add some more context.

In this article we'll be covering createStore. This is the function that sets up the redux store and provides access to getState, dispatch, subscribe, and replaceReducer.

A note on embedded source: The most up to date redux source is available here. I'll be embedding source code as of when this post was written.

Three Principals

Before we get started we should reexamine the three principals of redux. Below is an excerpt from the redux docs.

Single source of truth

The state of your whole application is stored in an object tree within a single store.

State is read-only

The only way to change the state is to emit an action, an object describing what happened.

Changes are made with pure functions

To specify how the state tree is transformed by actions, you write pure reducers.

Takeaway

The important bit here is that everything is done with pure functions. As a result of this we prevent consumers from doing things like calling dispatch or getState from within a reducer.

As we talk about createStore we'll notice that the majority of the code enforces these constraints.

Function Signature

Now that we've introduced this, let's dive into createStore. We'll start with the function signature.

This takes three arguments reducer preloadedState and enhancer.

Reducer

The first one is hopefully straightforward. This is the rootReducer for your application. It's a pure function that takes a state and an action and returns a new state. It's most likely generated by combineReducers which we'll cover in part 2.

preloadedState

This is simply the state you'd like to start with. Most likely if you're using preloadedState you're doing one of two things. You have a server side rendered the page, or you have a data payload coming down with your source. Either way you  have data you want loaded into redux as soon as the app is instantiated.

If you're not doing one of these you should probably just dispatch an action after the store is created.

enhancer

I sometimes like to call this the enchanter because it really does some magic. The most common uses for this are the redux devtools or adding middleware, another thing we'll see in part 2.

Argument Validation and Enhancing

The next lines of code are mostly argument validation. You can read the full source below but here's what it's doing:

  1. Ensures you've passed at most three arguments
  2. Allow the user to only pass reducer and enhancer. If this happens it swaps the argument order.
  3. Ensures the reducer is an actual function.

There is one very interesting thing going on here though. You can see it on line 22 in the gist above.

enhancer(createStore)(reducer, preloadedState)

If an enhancer function is provided it calls it with createStore as an argument. This function then returns a new createStore function which only takes reducer and preloadedState.

This pattern is very common in the redux source, wrapping functions in other functions. In fact I like to say it's functions all the way down.

This is also how redux devtools works. It wraps the createStore function so it can inject wrappers around every exported function and listen to their inputs and outputs. We won't dive too deeply into this, but you can check out that source here.

Internal State

Before we get to the function declarations, createStore needs to set up some internal state.

Let's walk through each of these.

  • currentReducer - The root reducer redux calls.
  • currentState - A reference to the key/value object that represents state
  • currentListeners - This is a list of functions that have subscribed to redux.
  • nextListeners - This is a reference to the updated listeners before a dispatch.
  • isDispatching - Effectively a semaphore to block us from dispatching more than one action at a time.

If these don't make total sense yet, especially the latter three, don't worry. We'll cover these more later.

Function Declarations

We're still inside of createStore but now we're declaring functions that will be used throughout the lifecycle of the store. Some of these will be returned as part of the store, others will only be used internally. I'll label each as we're going.

ensureCanMutateNextListeners

This function seems inconsequential but it's actually very important. Redux supports pub-sub or publish-subscribe. We have no invariants around when a consumer may decide to subscribe to our store or unsubscribe from it. As a result nextListeners needs to be mutable at all times.

Consider, a listener might even unsubscribe as a result of being called!

As a result of this we only call listeners in currentListeners and let any new subscriptions modify nextListeners.

Under the hood, this works because Array.prototype.slice returns a new reference to the list so currentListeners and nextListeners point to different arrays.

getState

This is a fan favorite, this is the function that returns state. It get's returned as part of the store and returns a reference to the current underlying state object.

You'll notice there's an invariant here that prevents us from calling this function during a dispatch. There's a good reason for this. When reducers are combined they only get a slice of the state, more on that in part 2. We don't want a reducer to get access to this function and derive it's state from another namespace state tree. As a result this throws if we're in the middle of a dispatch.

You may be thinking, but I call getState from a thunk! We're specifically referring to the plain action dispatch, not thunk dispatch. We'll cover thunks in part 2.

Subscribe

The next function created is subscribe. This is what notifies listeners that something in the store has changed. This function is returned as part of the store.

Hopefully this pattern is starting to look familiar. Subscribe takes a function that gets called every time the internal state updates. It also does the following validation:

  1. Make sure the argument is actually a function.
  2. Make sure we're not attaching a subscribe in the middle of a dispatch.
  3. Make sure we can mutate listeners, otherwise we couldn't subscribe!

It also creates some internal state, isSubscribed. This is used in the unsubscribe function it returns.

Unsubscribe.

We've covered how we start listening to things, we call subscribe. But how do we stop? Well subscribe conveniently returns a function called unsubscribe that allows us to stop paying attention to redux.

This function is very similar to subscribe. It checks isSubscribed (internal state) to see if it's already been called, and does some validation to make sure we're not currently dispatching.

Then it removes listener from the list.

mapStateToProps

You might be thinking. I've been using redux for years, I've never called these functions. That's because most integration libraries will abstract all this away for us. In the case of the react-redux library this is taken care of in the <Provider /> component. We won't delve too far into that library but you can check out the full source here.

Dispatch

It's worth a reminder that all of this is still being done in the context of the createStore function. The next function is the most important one, dispatch. This is the thing that updates the store and notifies listeners. Let's walk through it.

Validation

Like every other function we've encountered. dispatch starts with a bunch of validation.

You can see above it's enforcing the following rules

  1. We're only dispatching a plain object.
  2. That object has a property called type.
  3. We are not currently dispatching.

But once again you might be thinking, but I dispatch functions! This is taken care of by the thunk middleware, something to look forward to in part 2 of this article.

Business Logic

Now that we've made it this far, this function is probably much less impressive than you expected. Here's what it's doing.

  1. Set the isDispatching semaphore to true
  2. Call currentReducer with currentState and action. This returns a brand new state for our listeners.
  3. Update listeners with the next set.
  4. Loop through the list of listeners and call each of them.

This is also where it's important that we keep currentListeners and nextListeners separate. While the reducer must be a synchronous pure function, there is no such invariant around any given listeners.

A listener may be async, take a long time, modify some global state, etc. It might even unsubscribe as a result of being called.

Two More Functions

We've now covered the bulk of the business logic in createStore. There are now just two functions we need to mention, replaceReducer and observable.

replaceReducer

The function itself seems pretty straightforward, we take a new reducer and replace the one we use in the store. Then we dispatch an action announcing this has happened.

This action will also have the "side effect" of copying over any state that the new reducer needs to know about.

Why would you need this? This function is particularly useful for code splitting. I wrote an entire article about how to code split a redux store so I'll leave that as a followup.

Observable

This last bit I've never actually used, so I don't think I'm qualified to go into too much detail about it. Effectively it adds support for to support observables, a proposal to ecmascript. It's  just a simple wrapper around subscribe/unsubscribe.

Wrapping Up

The very last bit in createStore is initializing the state tree and exporting functions.

This is how users get access to store.getState, store.dispatch, store.subscribe, and replaceReducer.

Next Time!

In our next article we'll be covering a lot of the tooling around redux. We'll talk about applyMiddleware, bindActionCreators, thunks, and more!

Part two is now available here!


Ads

These ads help me pay to keep this site up. Feel free to buy, watch, listen or ignore these like any other ad.

Show Comments

Get the latest posts delivered right to your inbox.