Reading Code - Redux

January 12, 2022 6 minute read

Few things have obsessed me throughout the years as much as programming. Maybe the only other activity that held such an important role in my life was Brazilian Jiu-Jitsu. Back when I rolled on the ground more than I wrote software, I prepared for competition by training a lot and watching a lot of tape.

The motivation is simple - you need to train your body to perform and recognize certain moves. But you have a limited set of training partners and sooner or later you learn everyone’s tricks.

You still need a source of new information to inspire you and keep you creative or you will limit your growth. You need to know what people outside of your environment are doing.

Reading Code to Improve

I kept the habit of watching other people’s work after I put my gis (the uniform you wear in BJJ) away. I believe the best way to improve as a software developer is to write a lot of code and read a lot of code.

We are limited to the number of technologies and challenges we can get exposed to mostly because of time. Mastering a language takes time, developing a complex system takes even more. So we can’t rely only on doing. But lucky for us, we have a hoard of knowledge to learn from in open source.

Sometimes I browse other people’s code just looking for inspiration or an idea to “steal”. Sometimes I’m curious about a language or a technology and I find a popular project on GitHub written in it, to see how it’s used in the real world.

The syntax is easy to grasp but design patterns and idiomatic practices require you to see some real code

Diving into Redux

This time I dove into Redux’s implementation. We had one of those state management conversations at work and I realized I’ve never read its source code.

What I usually do is start from one of the public APIs of the project. I follow the implementation from function to function until I understand how it works. This helps me to understand the patterns, naming conventions, and how the project is structured.

I opened Redux’s createStore function and I found something interesting. The function was overloaded and this is a practice that you can rarely see in TypeScript projects.

Before we look into the implementation let’s talk about overloading in general. It allows you to have multiple functions that share a name but differ in parameter types and return types. An overloaded function can also have separate implementations.

function add(a: string, b: string): string

function add(a: number, b: number): number

function add(a: any, b: any): any {
  return a + b
}

The createStore function in Redux is a perfect example of using the same function to achieve different behavior based on the passed arguments.

export default function createStore<
  S,
  A extends Action,
  Ext = {},
  StateExt = never
>(
  reducer: Reducer<S, A>,
  enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext

export default function createStore<
  S,
  A extends Action,
  Ext = {},
  StateExt = never
>(
  reducer: Reducer<S, A>,
  preloadedState?: PreloadedState<S>,
  enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext

export default function createStore<
  S,
  A extends Action,
  Ext = {},
  StateExt = never
>(
  reducer: Reducer<S, A>,
  preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
  enhancer?: StoreEnhancer<Ext, StateExt>
): Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext {
  // Implementation is here...
}

When the overloading is trivial you can make sense of the differences by looking at the arguments. But my first thought when looking at this implementation is how simple it could’ve been if it was split in smaller functions.

There are two ways to create a store. You either pass a reducer and an optional enhancer or you pass a reducer, preloaded state, and an optional enhancer.

The library saves the users from some complexity, by exposing a single function that can be used for different purposes. But in doing that, the first 40 lines of the implementation are type checks to make sure that the function has been called correctly.

if (
  (typeof preloadedState === 'function' &&
    typeof enhancer === 'function') ||
  (typeof enhancer === 'function' &&
    typeof arguments[3] === 'function')
) {
  throw new Error(
    'It looks like you are passing several store enhancers to ' +
      'createStore(). This is not supported. Instead, compose them ' +
      'together to a single function. See https://redux.js.org/tutorials/fundamentals/part-4-store#creating-a-store-with-enhancers for an example.'
  )
}

if (
  typeof preloadedState === 'function' &&
  typeof enhancer === 'undefined'
) {
  enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
  preloadedState = undefined
}

if (typeof enhancer !== 'undefined') {
  if (typeof enhancer !== 'function') {
    throw new Error(
      `Expected the enhancer to be a function. Instead, received: '${kindOf(
        enhancer
      )}'`
    )
  }

  return enhancer(createStore)(
    reducer,
    preloadedState as PreloadedState<S>
  ) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
}

if (typeof reducer !== 'function') {
  throw new Error(
    `Expected the root reducer to be a function. Instead, received: '${kindOf(
      reducer
    )}'`
  )
}

By providing multiple functions those checks can be cleared and we can rely just on the static types instead. That way we handle potential problems at compile time and we can provide faster feedback to the callers.

Overloading makes the function definition way too complex. If I were to follow only the implementation I would think that I can provide a reducer and two enhancers, but that is explicitly forbidden in the first conditional check.

Devoting that many lines to validate that the function is called properly looks like a problem with its API to me.

Overloading Breaks SRP

Is overloading a good practice in general? I’d say it’s a matter of taste and agreement.

It’s a more common practice in some languages, but it mostly comes down to what you prefer as a general practice. My friends who work with JavaScript’s distant cousin Java, use overloading on a daily basis. But I’ve never seen it used in production in a JS project (well, now I’ve seen it in Redux).

This is more of a philosophical conundrum but I think that overloading breaks the single responsibility principle to a certain extent. You could argue that it’s the same function used with default values. But to me, overloading is a signal we are forcing a function to do more than it should.

The problem with multiple responsibilities is not just an academic one. We favor the single responsibility principle because it makes things simpler. I think the createStore function can easily be split into two functions, leaving the enhancer as an optional parameter.

As a side-effect, you can have a more intuitive function name for the case with the preloaded state like createStoreWithState or something in those lines.

Personally, I’m not a fan of overloading and unless it’s a practice in the team I will try to avoid it. The implementation could be made simpler by providing different functions that users can import depending on the way they want to bootstrap their store. They would all delegate to the same function underneath.

Always More Context

The authors have made a trade-off by handling more complexity inside the function’s body and giving a simpler API to the caller.

Personally, I’d rather have multiple functions that call one another and have shorter implementations instead, but one can argue which is simpler. More functions add more layers. The overloaded definitions still provide a good user experience and everything is handled in a single function.

But there’s another reason to use overloading. Perhaps they and the community were happy with the API and didn’t want to change it. So that was the simplest way to add types without altering the implementation.

There’s always some additional context that can’t be found in the code and I guess Redux is not an exception.

Tao of Node

Learn how to build better Node.js applications. A collection of best practices about architecture, tooling, performance and testing.