Jake Teton‑Landis

Perfection enthusiast and byte craftsman splitting time between Miami, FL and New York, NY.

Interested in human productivity, big levers, and design.

GithubMastodon
TwitterLinkedIn

Work in progress

Sufficiently advanced reducers

In which we explore a timeless concept across a gradient of possibility.

May 2022

Today I’d like to share with you some thoughts about reducers. →

At this point, it’s a well-explored concept in the React community. We’ve been talking about reducers since 2015

A reducer is a way to express how a program changes over time as a pure function.

This idea was popularized by Dan Abramov in his talk at react-europe 2015 where he introduced Redux (2015), a library for managing state in React apps using reducers inspired Elm.

Dan’s goal:

[...] to create a state management library with minimal API but completely predictable behavior, so it is possible to implement logging, hot reloading, time travel, universal apps, record and replay, without any buy-in from the developer.

The function signature looks like this: (currentState, action) => nextState. It takes two parameters: the current state of something, and an action to update that state, and it returns the next state.

Let me tell you, that talk moved the yavascript world. In 2015, Airbnb beginning to adopt React (I had just rebuilt the homepage search bar with it), but our most complex UI code (”Manage Listing”) was still in Backbone, making incremental changes to the DOM using jQuery or Mustache templates. Keeping the DOM in sync with data in those apps was a chore – so a system for writing apps that provided time travel and perfect observability debugging was magical. We flocked to React and Redux. It became The Way.

All kinds of apps are written around

Prologue

Our story begins, as so many do, with a brief tutorial on React, a popular library for building user interfaces.

  • start with React useState

    • Effects - reactions to state change

    • Purity, referential equality

  • Implement useState as a useReducer

  • Explain the appeal of reducers/Redux

    • Popularized by Dan in his famous talk

    • Show logging actions

    • Show time travel

  • Complain about boilerplate

    • Action constructors

    • Matching actions in reducers

  • Look at Redux Toolkit, Kea

    • Developing a short-hand that brings action and reaction closer together in code

    • While preserving debugging, time travel, effect scheduling via referential equality

  • Propose a JSON path reducer

    • Set/update actions

    • Show how we retain the nice things above

  • Complain about the verbosity, use Proxy to build actions

    • Compare to Immer
  • Limitations of referential equality for effect scheduling

    • Memory use

    • Selector performance

    • Over-subscription

  • Explicit subscriptions (in the JSON path reducer)

    • Build a trie

    • Too much detail here?

    • Still actually deferentially equal!

    • Now we can time travel the subscriptions

  • Subscribe via proxy

    • Mobx style
  • Allow mutation with time-travel

    • Inverse operations

    • Still logging all changes

    • Can still undo!

    • Turn off logging, inverse operations in production.

  • Review: we love that the state in this container is so debuggable, can be time traveled, and triggers minimal effects when changed

  • What about the rest of our program?

    • Code, asset loading

      • Model this in the reducer too
    • Browser events

    • DOM mutations from outside the framework

      • Extensions
  • Can we time travel every kind of thing?

  • sure, but we need a more complicated reducer

    • Determinism

    • Outside influence

      • Need to remember indeterminism like Math.random

      • Can’t forget any indeterminism if we want to time travel

  • QuickJS

    • Stack based VM

    • reducer: the code WebAssembly.module

    • State: the memory WebAssembly.arraybuffer

    • ASYNCIFY: pause the “reducer” for snapshot

    • We control the non-deterministic inputs, eg provide Math.random

  • Programs that can’t compile to WebAssembly

  • Von Neumann architecture

    • AMD64 instruction set: the actions

    • AMD64 CPU: the reducer

    • “Virtual memory”: the state

  • This isn’t theoretical.

  • Time travel debugging for Firefox

  • Tradeoffs

    • Complexity

    • Expressiveness

    • Performance - code size, memory, CPU, reactions

  • Choice