Reactors

In the chapter on selectors, I covered how we can use them to efficiently derive specific answers from our state. As it turns out, this model is so powerful that once you've used it, you start to imagine containing all sorts of logic in this way. Imagine, for example, if instead of just using selectors to read data, we used them to determine if we should dispatch another action!

I've started using this approach a lot. I'm going to refer to these special selectors as "reactors." They're just selectors that check for certain conditions, and if those conditions are met, they can dispatch other actions. This approach enables a whole slew of interesting use cases. Once our state is in Redux, URLs and all, we can do all manner of things with this pattern such as:

  1. Trigger redirects within our app.
  2. Clearing out state once a session expires.
  3. Fetching updated data if it's stale.
  4. Automatically and seamlessly retry failed requests.
  5. Trigger dialogs.
  6. Trigger browser APIs.

Conceptually it needs to work like this:

  1. We need to somehow "register" these reactors to be evaluated regularly.
  2. Each reactor needs to be evaluated after each action, much like what connect() does. So something needs to subscribe to the store and run all the reactors and dispatch any results.
  3. A reactor needs to be able to somehow cause an action to be dispatched.
  4. The reactors should either return something "falsy," like null or just not return at all (undefined). Or they should return something dispatch-able, which could be a plain action object, or the result of calling an action creator.

Simple example:

import { createSelector } from 'reselect'

const sampleReactor = createSelector(
  selectSomething,
  selectSomethingElse,
  (something, somethingElse) => {
    if (something && somethingElse) {
      return { type: 'SOMETHING_HAPPENED' }
    }
  }
)

How could you do something like this? It might be tempting to use connect() and your app's root component to run your reactors. But that doesn't feel quite right because these reactors have nothing to do with the UI specifically, so putting them in a component feels a bit odd. Instead, we can call subscribe() on the store after we create it. We can aggregate our various reactor functions in the same place we create our store, and run them after each action to see what, if anything, needs to be dispatched next.

Let's see what that could look like:

import reactors from './reactors'
import ric from 'ric-shim'
const store = createStore(rootReducer)

store.subscribe(() => {
  const state = store.getState()

  // We can use `Array.prototype.some` to grab
  // the first reactor that returns something
  // truthy. But as soon as it returns, it will
  // stop iterating.
  let nextReaction
  reactors.some(reactor => {
    const result = reactor(state)
    if (result) {
      nextReaction = result
      // returning true will stop
      // the loop
      return true
    }
  })

  // if we found something
  // schedule it for dispatch
  if (nextReaction) {
    // We'll use requestIdleCallback where
    // available. This `ric-shim` library
    // I'm using here will fallback to
    // setTimeout(() => {}, 0) if needed.
    ric(() => {
      store.dispatch(nextReaction)
    })
  }
})

A few things to note about the example above:

  1. We look for and grab only the first "truthy" result of running the known reactors.
  2. We assume we can dispatch the result directly.
  3. To avoid nested dispatching of actions, which is not good for performance, we schedule the next dispatch to occur on requestIdleCallback. This lets the browser to handle it as soon as it's not busy. This can improve performance and avoid "jank" when scrolling, etc, because a dispatch will inevitably cause a lot of other code to run, including the render function of potentially many components that update the DOM. The ric-shim library uses it if available, and falls back if not.

A big caveat

Infinite loops! So, as you can imagine, using this approach makes it pretty simple to create a scenario where your application is constantly reacting to the same state. Since a specific state causes these actions to be dispatched, you'll be stuck in a loop unless the dispatch immediately removes this state.

To make matters even worse, because we were kind enough to use requestIdleCallback the browser will not just run out of memory and crash. Instead, the app will happily sit there and wait for the previous dispatch to finish before politely scheduling yet another dispatch of the exact same thing, forever.

So the key thing to beware of is that you must immediately dispatch an action that changes the state that triggered the action. You must also make sure your reactor function checks for that change before returning anything.

As a quick tip, the best way to debug these situations is to look for the action that is being dispatched continuously, and then inside of the reactor that causes it, add a debugger right before returning. This will let you inspect the conditions that it is checking against at that exact moment to determine why it thinks it needs to trigger the action again.

Handling time-based actions

There's just one problem with this code so far. Even though subscribe() callbacks run whenever an action has been dispatched, selectors will only be re-evaluated if the results of their input functions have changed. Why does this matter? Well, let's assume we're building something like Twitter and we want each tweet to show a relative time, such as "just now," "5 minutes ago," etc. If all we did was add the idling approach described above, those relative times would only be updated if some state had changed. So if someone left their computer open, as they very well may on a twitter client, that information would not update unless some state changed that would change the outcome of that calculation. When dealing with relative time, we're not just formatting a timestamp, we're comparing a timestamp to whatever the current time is.

One approach would be to use setInterval in a component that renders the relative time to address this. But what if we want to build an app that checks how stale its data is and automatically re-fetches every so often? This scenario would also require that we calculate a relative time, but now we're dealing with state directly, and we're not writing a component. Ideally, we want to be able to calculate relative time in a reactor. At first, you may be tempted to use Date.now() in a selector's result function. But, as we said, selectors only run their result function if their input functions have changed. Therefore, the result function that you write cannot depend on anything other than data that comes from its input selectors. In other words, the function must be "deterministic." A deterministic function is one that whenever you call it with the same inputs, always returns the same result. A nice side-effect of following this rule is that it keeps your selectors easy to test, since anything it operates on is being passed in as an argument.

So how might we address this problem? The most straightforward answer is to track current "app time" as part of our Redux state.

It turns out this is incredibly easy to implement. We can simply timestamp each action by adding an "app time" reducer. Since a reducer's job is to return the new state, if we don't care about the old state, we can use the Date.now function in place of our reducer functions, like this:

import { combineReducers } from 'redux'

const rootReducer = combineReducers({
  appTime: Date.now
  // otherReducer,
  // yetAnotherReducer...
})

This does a few things for us:

  1. It makes it possible to set current app time as an input to our selectors while keeping our selectors pure and deterministic.
  2. This also means we should be cautious what selectors use the time as an input since they will re-evaluate on every action dispatch.
  3. We can now also write code that ensures something is dispatched at least every so often to ensure that all of our reactors that care about application time will be re-evaluated.

Letting your application idle

Much like you may let the engine of a gasoline car idle at a stop sign to keep it ready to react to pressing the gas pedal, we can make our application idle, too. I tend to set things up so that if no actions have been dispatched in the last 30 seconds and the browser tab is "active," then it will dispatch an { type: 'APP_IDLE' } action.

First, let's ignore the "active tab" bit and just consider how we could write code that would dispatch an action if nothing has happened for a certain period. It sounds a lot like the idea of a "debounced" function, right? A debounced function, when called continually, will only run once you're stopped calling it and a certain time has passed.

So we can combine store.subscribe() with a debounced function like this:

import { debounce } from 'lodash' // or something similar

// assume we've created our Redux store here:
const store = createStore( ... )

// a simple function that dispatches an idle action
const idleDispatcher = () => {
  store.dispatch({type: 'APP_IDLE'})
}

// create a version of the function that is
// deBounced to 30 seconds
const deBounced = debounce(idleDispatcher, 30000)

// Now this will run *each time* something
// is dispatched. But once it's been 30 seconds
// since something has happened. It will cause
// its *own* dispatch. Which then start the cycle
// over again.
store.subscribe(deBounced)

With just that you can now be sure something will be dispatched in your application every 30 seconds; this will ensure that your time-based selectors and reactors have a chance to potentially react to the new state.

The other cool thing about this is that you no longer need to do any setTimeout or setInterval code anywhere else in your app just to be able to keep things like "relative times" up to date. Instead, we've centralized all that timing-related code into just another piece of our state.

Ok, let's get a bit fancier. What if we want to make it so that it only idles if the browser tab is active? This is really just about being nice to our users' phone batteries. If they're not actively looking at the tab, then we don't need to be re-evaluating things in the background; let's not waste cycles.

Doing this may sound difficult, but fortunately, the browser handles a lot of the complexity for us. As it turns out requestAnimationFrame, while designed primarily for things like refreshing <canvas /> elements, is smart enough to stop running its callback if the tab isn't visible. This is because the browser shouldn't constantly animate a canvas at 60 frames per second when it isn't even visible! But we can also tap into this functionality to make our "app idler" a bit smarter. In fact, we can get even fancier and also use requestIdleCallback to make sure the user isn't in the middle of scrolling.

Let's see how this would look:

const idleDispatcher = () => {
  store.dispatch({ type: 'APP_IDLE' })
}

// Create a version of the function that is
// deBounced to 30 seconds
const deBounced = debounce(() => {
  // The requestAnimationFrame ensures it doesn't run when tab isn't active
  // the requestIdleCallback makes sure the browser isn't busy with something
  // else.
  requestAnimationFrame(() =>
    // this timeout option for requestIdleCallback is a maximum amount of time
    // to wait. I'm including it here since there have been a few browser bugs where
    // for various reasons browsers fail to trigger idle callbacks without this argument.
    requestIdleCallback(idleDispatcher, { timeout: 500 })
  )
}, 30000)

store.subscribe(deBounced)

Sweet, right?! Very little code, yet it enables a lot of really cool behavior for our app.

The really neat part is when, as a user, you do switch back to the tab, and see it "jump to attention" if you will. All of a sudden any time-based code will wake up and trigger fetches and whatever is necessary to bring things back up to speed without any user interaction required!

What about other async approaches like redux-saga and redux-loop?

Personally, I've settled on the patterns described above for a few reasons. First, I find them easy to wrap my head around. Using selectors to do what I like to refer to as "spreadsheet programming" just fits my way of thinking. But, I think my readers will perhaps wonder why I'm not recommending some of the well-established approaches to this problem. So I will discuss them briefly.

People have certainly created alternate solutions to describing more complex behavior in Redux. Most notably two projects:

  1. redux-saga
  2. redux-loop

Redux-saga uses generator functions to let you describe "side effects" as actions are happening. It is installed as a middleware. Sagas are designed to let you describe complex asynchronous workflows. If something is difficult to implement with redux-thunk, you might use a saga. In that case, instead of writing an asynchronous action creator, your app would dispatch synchronous action objects, but then saga middleware would look for certain actions which would trigger various side effect tasks to run. So you write your asynchronous procedures as "sagas" and combine them into a root saga that gets registered as middleware, then you init your sagas. It's a clever approach, and it's based on defined patterns in computer science.

Personally, I have a strong aversion to complexity, and after reading the redux-saga documentation I was been so turned off by the number of new concepts and procedures it adds, that I never bothered to actually try using it. The documentation is well-written; redux-saga was obviously created by some very smart people, but in my opinion it simply adds too much to what was previously a simple "predictable state container." After seeing the example code, it doesn't seem like it leads to a net decrease in complexity. It has its own patterns and API surface to learn, and contains approximately four times more code than Redux. To be clear: I'm not disparaging redux-saga or its authors. I have no doubt that it can be used to build great applications if it matches your mental model, but it doesn't match mine.

Redux-loop takes a different approach. It describes itself like this:

A port of the Elm Architecture to Redux that allows you to sequence your effects naturally and purely by returning them from your reducers.

Loop changes the API for reducer functions. Instead of just returning state from your reducer you can optionally return the result of calling its loop() function. The idea is that you're returning something that describes the side-effect that should occur along with the state, thereby letting you "schedule" effects to be run by redux-loop. However, since it has to change the API for reducers, it is added to your application as a store enhancer. Also, to support this change it also needs its own version of combineReducers. Again, for me, this doesn't match my mental model. I love the simplicity of a redux reducer simply being responsible for updating the state that it manages. Regular Redux reducers (not the Loop version) are simple, pure, self-contained functions with a single responsibility. Unfortunately, redux-loop changes that. Redux-loop is smaller than Saga in terms of code size, but still is bigger than Redux itself and also requires a Symbol polyfill for older browsers which can be annoyingly large.

Redux is beautiful the way it is

Both redux-loop and redux-saga seem reasonable for what they're trying to do. However, my main complaint with both is the same:

Neither one allows me to use the current state of the application to determine what should happen next.

I don't want "side-effects" that are triggered by specific actions occurring. I want "side-effects" triggered by the current state. I want what I've been referring to as "spreadsheet programming."

In my experience, letting our state inform what should happen next grants us significantly more power than writing procedures that should occur in response to specific actions.

How?! Well, in the next chapter we'll talk about how to use these patterns to build incredibly fault tolerant, self-healing, applications.

Chapter recap

  1. I introduced the idea of what I call a "reactor" which is just a selector function that either returns nothing, or returns something that can be dispatched.
  2. Whenever you use the current state to trigger a dispatch you increase the risk of creating loops. To avoid loops when using this pattern, we must ensure that the action that our reactor returns will immediately causes a state change that the reactor checks for. If not, you'll have infinite loops.
  3. The best way to debug infinite loop scenarios is to insert a debugger statement inside the if statement in your reactor function so you can inspect the conditions that are causing it to dispatch again.
  4. We can track "app time" in our Redux state by adding a simple reducer that is just Date.now. This effectively timestamps each state change.
  5. We can select app time in our selectors to keep selectors deterministic and pure, therefore easy to test.
  6. If we want our app to be able to react to the time stored in Redux, we need to make sure something is dispatched every so often. We can solve this by adding an idle action that is called on a debounce to ensure something will be dispatched at the interval we set.
  7. We can optionally turn off idling when the tab is not active by using requestAnimationFrame. We can also make sure the browser isn't busy with a more important task by using requestIdleCallback.
  8. We discussed redux-saga and redux-loop and what I consider to be their primary weakness: they use your actions to cause side-effects instead of using your application state to inform what should happen next.

results matching ""

    No results matching ""