Journey to 0.x - Redux

October 30, 2022

This is the first installment in the Journey to 0.x series, where we find very old versions of popular software to better understand the deep concepts behind how they work.

Here’s a pattern I’ve noticed in software. You hear about a new platform or library. Over time you hear about it more and more often. Maybe you try it out for a small project, maybe not. But eventually you start a new job and this once-new software is the thing they use. So you learn the ins and outs, you copy/paste liberally and you get something working.

Fast forward a few months, or a few years, and you’re still working in this (now very established) library. You’re proficient in it. You might even have recruiters asking if you’d consider a new job specializing in this library. So you know the library well. But… do you really? Did you ever take the time to understand precisely, exactly how it works?

By now the library is many years old, and its codebase is a juggernaut. There’s absolutely no way you could ever dissect the code, even though it’s open source. It may seem like you’ll never be able to understand the fundamental mechanics of this platform. But wait. There’s one powerful tool on our side: version control.

We can rewind most codebases all the way back to the beginning, time-traveling into the past, to a time when the code was tiny, readable and totally comprehensible. The API may have changed since then… but often times, the core concepts have not.

* * *

I’ve found myself in this situation a few times, and this week I recognized that familiar feeling with another library I’ve been using for years: Redux. A simple concept, and one I understand well… But the core mechanics? The internals? I must admit, those are fuzzy to me.

The official repo goes back to version 0.1.0, so I started reading there. It remains fundamentally the same code until about 0.4.0, when “addons” are introduced, so I settled on trying out version 0.3.0. You can read the entirety of Redux at this point in history in these three files.

I fired up a code sandbox and created a basic “hello world”-style React app, that would hopefully illustrate the fundamental value of Redux: A parent component which stores state (in this case, a random number), and passes that state to a display component, and the function to set state to a button component. Conceptually it looks like this:

Parent component [keeps the state in this.state]
  ├─ > passes this.state.number to a NumberDisplay component
  ├─ > passes this.setNumber() to a Button component

(Here’s that code, by the way.)

Now obviously in such a tiny example, Redux would be overkill. But if you imagine a full app, with dozens of deeply-nested components, you quickly realize passing state and setState() functions around five or six levels deep is madness, and not scalable. So let’s add Redux here, just to learn how the internals work.

I copy/pasted the source for Redux 0.3.0 into my little example app, made a few small changes to the dependencies (PropTypes has since been removed from React, lodash has changed its node module structure, etc), and with surprisingly little effort I got it running in my modern React sandbox. You can see it here and follow along.

The app now looks like this:

Parent component [initializes Redux]
  ├─ NumberDisplay component [connects to Redux store]
  ├─ Button component [connects to Redux actions]

Plus our new Redux library and files
  ├─ redux/
  ├─ _store.js
  ├─ _action.js
  ├─ _constant.js

You can already see how this would be a major advancement compared to passing state and setState around endlessly.

Okay, but how does it work? What precisely are the mechanisms responsible for Redux’s behavior? Let’s look at the implementation:

Parent component

The parent component, App, needs only one line of code (two if you count the import):

import Root from "./redux/Root";

@Root

This is the Root decorator that instantiates the Redux stores and, in modern Redux, is done with a Provider container. But in 0.x Redux, this does the same thing.

Connected components

  <ReduxContainer actions={ setNumber }>
    {(props) => (
      <button
        onClick={() => props.setNumber()}
      >
        generate
      </button>
    )}
  </ReduxContainer>

The two connected components in my app, NumberDisplay and Button, are each wrapped in a ReduxContainer parent component. Similar to the modern Redux equivalent, which uses a special Redux function called connect(), this parent component hooks your component up to the Redux system and passes the actions and data down as props. Unlike modern Redux, though, all of your data and actions are passed down, not just the ones you specified with mapStateToProps or mapDispatchToProps. Also, unlike modern Redux patterns, you instantiate your stores right here, instead of at the root component with configureStore/combineReducers, or similar.

The Redux library

redux/
  ├─ Container.js
  ├─ Root.js 
  ├─ createDispatcher.js 

So we instantiated Redux with @Root, which came from Root.js. And the two connected components both imported ReduxContainer from Container.js. Container.js gets most of its internals from createDispatcher.js. And.. that’s all there is to it. That’s Redux! The shape of our action (in the _action.js file), store (_store.js) and constant (_constant.js) are all very simple, but maybe still mysterious since they seem to be dictated by the Redux internals.

So let’s follow the path of our data, from creation, to update, to return, within the Redux system. And then perhaps we’ll finally feel like we understand Redux.

Initializing

The root component, initialized with @Root, is straightforward. The majority of its work is this line, where it calls a new createDispatcher instance from the createDispatcher.js file. That’s about it. The only other thing it does, really, is stick that createDispatcher, now just called this.dispatcher, into its React context. Note: this is an older, now-deprecated version of React context! React doesn’t want you to use this anymore. But the function getChildContext() is the old way to ensure that all child components will have access to the createDispatcher library.

Create dispatcher

The createDispatcher function is the most complicated part of Redux, but can be broken down into three key tasks it accomplishes.

Firstly, it creates three local variables, observers, stores and currentState. Those get populated initially by the actions and stores passed to ReduxContainers across the app (<ReduxContainer stores={numStore}> for example). In modern Redux, this is where all the configureStore and mapDispatchToProps data gets collected and stored.

Secondly, it contains all of the utility functions for updating the currentState, and notifying the observers. I don’t see anything too unexpected here. It might be a little difficult to parse the code, since it relies on a few shorthand techniques and the lodash mapValues function often, but the functions themselves are named clearly and you can probably get a good idea what they do by just reading the names.

Finally, it exports wrapActionCreator and observerStores, which will then become available through context to every component wrapped inside a ReduxContainer. Those ReduxContainers will use those to call actions, and return stores, respectively.

Data flow

Let’s use debugger to follow exactly what happens when we dispatch a new action, in our case, clicking the Button to generate a random number.

Clicking on the button triggers props.setNumber(). You might think setNumber() is the action we defined in _action. But no, if you inspect it, you’ll find it’s actually a wrapped action creator, which was done by the wrapActionCreator function in createDispatcher. What’s that mean? Well it now means the props.setNumber() function looks like this:

function dispatchAction(...args) {
  // Editor's note: actionCreator in the next line is now our original setNumber() function
  const action = actionCreator(...args);
  if (typeof action === "function") {
    action(dispatch, currentState);
  } else {
    dispatch(action);
  }
}

Why wrap this? Well, this way you’re able to pass an action that either returns another function (useful if you want to do things asynchronously, and then manually trigger the state update (dispatch) when you’re done), or returns a simple object to be dispatched. You’ll notice our _action.js function does the simpler thing and just returns a simple object:

return {
  type: SET_RANDOM_NUMBER,
  number: Math.floor(Math.random() * 100),
};

So all that happens is the object above gets dispatched. dispatch calls computeNextState, which I believe to be the actual core of what Redux is, and is therefore worth looking at in more detail.

function computeNextState(state, action) {
  return mapValues(stores, (store, key) => {
    const nextStoreState = store(state[key], action);

    return nextStoreState;
  });
}

So simple, yet so complicated. It takes state, which for us looks like {numStore: {number: 34}}, and action, which for us looks like {type: SET_RANDOM_NUMBER, number: Math.floor(Math.random() * 100)}. Then it runs an array of our stores through lodash’s mapValues, which just iterates over all the stores, handing over the store and its key. Then the big moment. const nextStoreState is calculated by calling:

store(state[key], action);

What does that do? Well in our example, our store, which was exported from _store.js, looks like this:

function numStore(state = initialState, action) {
  switch (action.type) {
    case SET_RANDOM_NUMBER:
      console.log("setting num", action.number);
      return {
        number: action.number,
      };
  }
}

So in modern Redux, that’s your reducer. And now we finally understand why reducers are functions! Our reducer is called by Redux when it runs that line: store(state[key], action). We pass our reducer the current state (state[key]), and then the action ({ type: SET_RANDOM_NUMBER, number: ... }). Then the reducer itself does the work, and returns the new state. That’s it. You can think of Redux as just being a bunch of fancy state-handling functions built around this core concept: You can make up your own actions that do things, and your own reducers that handle actions, and just hand them both off to Redux to take care of hooking all of that up and storing/returning state in a predictable way.

Once the new state has been calculated, it’s passed through to the observer object which is already being passed down to all connected components via the ReduxContainer component. And then that update is treated like any other React prop update. No need to get fancy, the data flows like normal props from here out.

* * *

So what can we take away from this? Zooming out further, we can see that the shape of _action, _store and _constant are mostly that way only due to convention! We didn’t really need a constant… it’s just convention. We didn’t need to separate the action and store… again, convention. When we strip away all of the distractions, and hyper-focus on the oldest, simplest version of a big software library like Redux, this is usually what we find. A simple concept, done very well, whose conventions and underlying mechanics, mysterious to us today, are actually very, very simple.

All it takes is a little time-travel to 0.x.