Skip to content

Instantly share code, notes, and snippets.

@christianalfoni
Last active April 3, 2019 10:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save christianalfoni/6dba44687c47edbdf754b5e64613767d to your computer and use it in GitHub Desktop.
Save christianalfoni/6dba44687c47edbdf754b5e64613767d to your computer and use it in GitHub Desktop.
The case for action based change detection

The case for action based change detection

There are several approaches to detecting change, for example:

  1. Mobx, Vue js, Overmind JS: Mutation detection using getters/setters or proxies.

  2. Redux: Reference comparison, typically combined with immutability (Is previous value different than current?)

  3. Cerebral JS: Path matching. With single state trees you can match what paths components depend on with what paths are being mutated in the state tree

All of these approaches has costs:

  1. All access to state is intercepted and tracked. Both for mutation and for components accessing. Also logic for matching changes with a registry of accessed properties/proxies/observables is required

  2. With immutability (at least changing out references) has memory costs and also all components needs to do reference comparison on every change

  3. Custom API to change state and matching paths with a registry of components accessing paths

What is a change?

Typically we think of a change as actually changing out values, but it does not have to be that way. Instead of tracking actual value changes we can track the intention to change state. This can best be explained with Redux. Let us imagine we have a counter and want to render whenever the count changes. Typically you would have to:

function MyComponent () {
  const count = useRedux(store => store.count)
  
  return <div>{count}</div>
}

When any state changes the callback will run to figure out if the value has changed, causing a new render. But what if we rather tracked the action that causes the count to change?

function MyComponent () {
  const state = useRedux("changeCount")
  
  return <div>{state.count}</div>
}

This component now subscribes to the changeCount action, meaning that whenever it triggers it will rerender after the action has performed its state change. At this point we have no idea what state actually changed, we only know what the intent was. Is that okay?.

You might argue that you conceptually care about the change of the value, but is that really the case? Let us see what benefits it gives us, if only to open our minds a bit.

The benefits

With this approach there is no need for immutabiliy to do reference checking, as there is no reference checking. That means we remove the overhead of producing new values and we completely remove all the evaluation of when a component needs to render. We do not need to compare the count with its previous value.

Additionally we can fine tune what a component cares about. Let us imagine a list of todos.

function Todos () {
  const state = useRedux("addTodo", "removeTodo")
  
  return (
    <ul>
      {state.todos.map((todo) => <Todo key={index} todo={todo} />)}
    </ul>
  )
}

This component will only render if our intention is to add or remove a todo. We completely avoid rendering if we change individual todos. Furthermore we can optimize each todo. Given an action of:

{
  "changeTodo": (index, update) => (state) => {
    Object.assign(state.todos[index], update)
  }
}
function Todo ({ todo }) {
  const state = useRedux({
    "changeTodo": (index, update) => (state) => {
      return index === state.todos.indexOf(todo)
    }
  })
  
  return (
    <li>
      {todo.title}
    </li>
  )
}

When an action triggers we can pass the payload and the current state to verify that any listening components actually wants to act on this change. That means in this scenario that all todos will indeed have their callback called and a check will be done. But this check is like a shouldComponentUpdate check with actual logic, not just a reference comparison.

Predictability

Often you get into situations when you do not know why a component renders or you fear it renders too much. This is especially tricky with reference comparison as nested changes causes changes to parent objects. With the approach explained here you do not care about state changes at all, you care about your intents. That means when actions are called a simple "console debugging tool" would give you the answer to why a component renders:

  1. My component is not rendering
  2. What intents does it rely on?
  3. What intents has been fired?

Thats it, no evaluation of the actual change itself.

That said, there is still a big drawback. You might implement multiple actions changing the same state and probably components looking at one action also cares about the other action changing the same state. That is something you would have to maintaine as a developer.

Proof of concept

So here is a link to a Sandbox with full type support: https://codesandbox.io/s/42xnpl7j54

How do we feel about this? :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment