How Redux Works - Part 1
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.
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:
- Ensures you've passed at most three arguments
- Allow the user to only pass
reducer
andenhancer
. If this happens it swaps the argument order. - 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 statecurrentListeners
- 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:
- Make sure the argument is actually a function.
- Make sure we're not attaching a subscribe in the middle of a dispatch.
- 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
- We're only dispatching a plain object.
- That object has a property called
type
. - 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.
- Set the
isDispatching
semaphore totrue
- Call
currentReducer
withcurrentState
andaction
. This returns a brand new state for our listeners. - Update listeners with the next set.
- 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!