Build Your Own Redux for React

Apr 17, 2018

Building and maintaining frontend infrastructure is a job I don't envy. The javascript ecosystem is notoriously mercurial, shifting every month it seems. And while React is by far my favorite framework, the different flavors of its usage are difficult to grasp, and articulated in different ways depending on whose blog your reading. I'd like to share my thoughts on this, and how I approaching writing frontend code as someone who's worked on distributed systems.

The Bad Way To Write React Applications

It's easy to clone a boilerplate repository, and just start writing code. But unless you're writing HelloWorld, your state-management logic is going to get wildly out of hand, as is your prop-passing.

This is bad code for many other reasons, but focus on the use of ref and function passing through the use of props.

This is where a lot of React developers discover Flux or Redux, and subsequently fall in love with the way it solves their problems. However, I was really frustrated with the way that a lot of developers explain it. There's a bit too much "install this, plug it in here, define your model there, and away you go." I'd prefer to know what design patterns I'm using, and why. In other words, I want control over my codebase. I'm willing to cede some control over updating, diffing, and rendering the DOM to React, but everything that happens before that is something that I'd like to understand.

The Very Basics

The most basic idea of Redux is that there is one way to update components. Any changes to a component made by a central datastore. Any changes to a component triggered by user interaction come through the same store. Like this.

This is a bit of an over-simplification, but the idea is that your view contains logic to render itself properly, but not to modifiy itself. All modifications should take place as actions, and should be thought of as operation-based. They're doing something to a model or state.

In practice, your main application logic might make asynchronous calls to a server to persist data, and update other views that are concerned with any data that has changed.

For example, selecting a user in your UI might look like this.

{
  "type": "SelectUserAction",
  "action": {
    "id": "bf3fcdd9-2c14-41ef-82c7-87c379c0aa94"
  }
}

Thinking of An Application as a Distributed System

It's easier to build systems - distributed or otherwise - if you maintain a separation of concerns. With this in mind, I find it useful to think of frontend components as a distributed system. Each component can trigger an action (event, message, HTTP request, RPC - however you like to think of messages being passed) that should change the state of something, possibly many somethings. In a distributed system, a message to update a username in a database will cause changes in the database, and it might trigger a message to update other models in other parts of the system, and even record a log of that change. Similarly, if you update a username in a component, you should emit a message to any components that might want to know.

If we imagine an admin UI where users can be updated, changes will be logged in an audit log, and stats about particular users can be accessed, the architecture diagram might look something like this.

Frontend on left, back-end on right.

You'll notice the diagrams are very similar. On the left side there are Views which publishers and/or subscribers. On the right side the UserHub, AuditHub, and StatsHub applications are publishers and subscribers, some by their relationship with the client application, others by their relationship to each other. Action, Event, and State are used somewhat synonymously in a lot of descriptions of Redux, but they're all thin veils on the idea of message passing between what behave as tightly synchronized state machines.

Building An Example Application

Let's build the above React application with our own implementation of Redux, so we can fully understand the design patterns it represents. (The full code for this post is available at https://github.com/vogtb/writing-react-redux-diy) We start out just by building out some basic models, and views, until our app structure looks similar to this.

src/
  Components/
    Containers/
      UserListContainer.tsx
    Views/
      AuditView.tsx.
      UserListView.tsx
      StatsView.tsx
      SingleUserView.tsx
    App.tsx
  Models/
    AuditLogEntry.ts
    DummyData.ts
    StatName.ts
    User.ts
    UserOperation.ts
    UserStat.ts
    UserStatus.ts
  Index.tsx

Then entry point for our application is Index.tsx, which loads and mounts the root React component App.tsx. So far our application works, and everything loads correctly.

But if we want to select a single user, and filter down the data in the other views, we're going to need something Redux-like in order to avoid cross-component, referential message passing. This is where we introduce a Store, which will do two things. 1) It will store the data that all our components will need, and 2) it will send and receive messages across components. While this could be done using references, the Store will achieve this by being a singleton, and binding functions inside the components themselves. Think of it as a combined messaging and storage abstractions for a distributed pub-sub system. It will look something like this:

class PrimaryStore {
  private subscriptions : { [index:string] : Array } = {};

  public dispatchFunctions(action: ActionType) {
    if (action in this.subscriptions) {
      for (let f of this.subscriptions[action]) {
        f();
      }
    }
  }

  public bindSubscription(actionType: ActionType, func : Function) {
    let actionTypeString = actionType.toString();
    if (actionTypeString in this.subscriptions) {
      this.subscriptions[actionTypeString].push(func);
    } else {
      this.subscriptions[actionTypeString] = [func];
    }
  }
}

const Store = new PrimaryStore();

We can then use an instance of PrimaryStore as a singleton, and bind functions to it as "subscriptions." Whenever we set a field inside the store, we send it an accompanying Action. When the Store mutates its own state, it calls all functions bound to that action type, as if they are subscribers to changes made by that action.

...
// Simple singleton using module namespace.
import {Store} from "../../Control/PrimaryStore";
...

class UserListContainer extends React.Component {
  constructor(props) {
    super(props);
    Store.bindSubscription(ActionType.SelectUser, this.pullState.bind(this));
    this.state = {
      store: Store
    };
  }
  pullState() {
    this.setState({
      store: Store
    });
  }
  render() {
    return (/*etc*/);
  }
}

In this example, we tell the Store singleton that anytime it receives a SelectUserAction, it should call pullState inside the UserListContainer. Conversely, if we want a component to be able to tell the store that a user has been selected, we have to bind that as well.

class SingleUserContainer extends React.Component {
  private selectUserAction() {
    Store.handleSelectUserAction(new SelectUserAction(this.props.user.getId()));
  }
  render() {
    return (
     

{this.props.users.getDisplayName()}

); } }

You'll notice that this requires us to write a new function on the Store to handle the SelectUserAction. Yes, this is one more place where you have to write code, but once you write a method to handle a particular action, you'll be able to use it for any further logic you'll need resulting from that action. Here's a more brief highlight of the code path for an update:

// In publishing component, SingleUserContainer
Store.handleSelectUserAction(new SelectUserAction(this.props.user.getId()));
// Inside Store.handleSelectUserAction
this.dispatchFunctions(ActionType.SelectUser);
// Inside Store.dispatchFunctions
if (action in this.subscriptions) {
  for (let f of this.subscriptions[action]) {
    f();
  }
}
// Inside subscribing component, UserListContainer, subscription was bound
Store.bindSubscription(ActionType.SelectUser, this.pullState.bind(this));
// Inside UserListContainer.pullState
this.setState({
  store: Store
});
// UserListContainer's state now has an updated version of Store,
// which will cause React's diffing algorithm to trigger a render.

When we make similar changes for all components subscribing to the SelectUserAction, we can filter down the amount of data on screen to only show data pertaining to a specific user.

When a user is selected, AuditView and StatsView are updated.

Benefits

How different is this from Redux? Not much. And how much better is this than prop-function passing, and holding references to other components? A lot. But here are the main benefits.

Testability

Now you can test components more predictability. Thinking of them as apps in a distributed system: to test an app you'd start it up, pass it a message, and check to see if downstream interactions were correct. To test your React components, you can create them with a mocked out Store, and initialize them to see if they respond to actions accordingly. With references and prop-passing you'd have to write ton of code, writing mock objects just to initialize the component you're testing, let alone test the component.

Extensibility

One measure of an implementation of a design pattern is how extensible it is. Can a developer look at your code, and recognize where they need to make changes to add or alter a feature? With a Redux-like configuration there are three places you need to add or alter code to connect components.

  1. Origin Component: The root component that will publish the action. For example, a tool bar will publish a "ToolToggleAction".
  2. Destination Component: The component that subscribe to the action, and similarly, the data your action is updating. In some cases this will also be your Origin Component. For example, a button emits a selection action, would want to subscribe to the action to disable itself while an asynchronous call is made.
  3. Store: The store that will hold your data for all components.

Even async calls to a database, or inbound push-messages from a server, can easily be recognized and slotted into these patterns. What's the difference between an action originating from a remote server, and an action originating from another component? Not a lot, really!

Creative Constraints

By forcing your inter-component communications to be in the form of actions, you're forcing yourself to think about how you model, manipulate, and store your data. It also gives you the option of writing behavior tests and component based unit tests that are based on actions in your application, rather than actions from a user's perspective, which are often vague, repetitive, and brittle. Ordering and sending actions through a globally bound store is a much more durable way to write tests.

Patterns Not Packages

I like Redux. I'll never complain about making frontend applications easier to understand, or about embracing frameworks that reduce the number of concerns developers have to deal with in an increasingly insane Javascript ecosystem. But I think it's easier in the long run to use the patterns that Redux represents, rather than using the packages that implement those patterns. Something like the following in a codebase doesn't mean much to me, and forces me to make a lot of assumptions.

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

I understand that it's connecting something, but it's also pulling unnecessary amounts of logic outside of TodoList. If it's not breaking the letter of the law of separation of concerns or encapsulation, it's certainly not following the spirit. Understand the pattern, skip the package.