Join The Dark Side Of The Flux: Responding to Actions with Actors

Have you ever wanted to respond to a change in your Redux store’s state by dispatching another action?

Now you know that this is frowned on. You know that if you have enough information to dispatch an action after the reducer does its thing, then it is a mathematical certainty that you can do what you want without dispatching another action.

But for some reason, you just don’t care. Maybe your store is structured in such a way that it is easier to send requests after an action is processed. Maybe you don’t want your actions or components to be in charge of fetching remote data for each new route. Or maybe you’re just a dark side kind of person. Whatever the reason, actors will allow you to dispatch with impunity.

Why you need actors

But before we jump into the details, lets review the alternative: connect. Here’s an example using react-redux:

class Counter extends React.Component {
  render() {
    return (
      <button onClick={this.props.onIncrement}>
        {this.props.value}
      </button>
    )
  }
}

const CounterContainer = connect(
  state => ({ value: state.counter }),
  dispatch => ({ onIncrement: () => dispatch(increment()) })
)(Counter)

const targetEl = document.getElementById('root')

ReactDOM.render(
  <Provider store={store}>
    <CounterContainer />
  </Provider>,
  targetEl
)

Instead of subscribing to the store’s state and rendering the UI explicitly, we’ve delegated the job of re-rendering to the connect function. The simplicity here is beautiful, but it presents a problem to us dark-siders: connect renders every state that the connected data passes through, even when we don’t want it to.

But don’t we want to render every state? Not if we’re dispatching actions after actions. For an example, what if we want to fetch out-of-date data after each action is processed?

store.subscribe(function fetcher() {
  const state = store.getState()
  const location = state.navigation.location

  switch (location.name) {
    case 'documentList':
      if (outOfDate(state.data.documentIndex)) {
        store.dispatch(data.document.fetchList())
      }
      return

    case 'documentEdit':
      if (outOfDate(state.data.document[location.id])) {
        store.dispatch(data.document.fetch(location.id))
      }
      return
    }
  }
})

Each time our subscriber receives a new state, an additional fetch action may be dispatched. This will cause your app to render once after an action is processed, and then again if fetch is dispatched. This is bad for performance, but more importantly, it will flash out-of-date data on screen — which looks terrible.

And that’s why we need actors!

What are actors?

Actors are a sequence of functions which are called each time your store’s state changes, with one important exception: they aren’t called in response to actions dispatched from other actors.

Each actor receives your store’s dispatch function and current state. The upshot of this is that if an actor dispatches an action, the next actor’s state will reflect the result of that action.

If this sounds at all complicated, it won’t after you see the implementation:

// An array of actor functions taking a `state` object and `dispatch` function
var actors = [ ... ]

var acting = false
store.subscribe(function() {
  // Ensure that any action dispatched by actors do not result in a new
  // actor run, allowing actors to dispatch with impunity.
  if (!acting) {
    acting = true
    actors.forEach(function() {
      actor(store.getState(), store.dispatch)
    })
    acting = false
  }
})

And now you can dispatch with impunity, safe in the knowledge that your final actor will receive a state object which takes into account any actions dispatched by previous actors.

Examples of actors

Knowing what actors are is half the battle; the rest is knowing how to use them. With that in mind, I’ve put together some examples of actors I use.

redirector(state, dispatch)

Redirecting is one of those things which probably should be handled before your store even knows about the new URL, i.e. in an action creator. But even so, I find this actor from the Unicorn Standard Starter Kit easier to grok than any action creator based approach:

function redirector(state, dispatch) {
  const {name, options} = state.navigation.location || {}
  const currentURI = window.location.hash.substr(1)
  const canonicalURI = name && ROUTES.generate(name, options)

  if (canonicalURI && canonicalURI !== currentURI) {
    // If the URL entered includes extra `/` characters, or otherwise
    // differs from the canonical URL, navigate the user to the
    // canonical URL (which will result in `complete` being called again)
    dispatch(navigation.start(name, options))
  }
  else if (name == 'root') {
    // If we've hit the root location, redirect the user to the main page
    dispatch(navigation.start('documentList'))
  }
}

fetcher(state, dispatch)

Say you have a route which renders data from your store, but that data needs to be fetched from an API before you can use it. How do you fetch this data?

  • Option 1: Call the API in the action creators which navigate to that route. Just hope that these actions are the only place you need the API.
  • Option 2: Dispatch fetch actions from your route Containers. Just hope that you don’t want to display any metadata about the fetch process – or you’ll get that pesky double-render.
  • Option 3: Make a fetcher Actor.

To me, it feels like the Dark Side is actually less dark than the alternatives:

function fetcher(state, dispatch) {
  const location = state.navigation.location

  switch (location.name) {
    case 'documentList':
      if (outOfDate(state.data.documentIndex)) {
        dispatch(data.document.fetchList())
      }
      return

    case 'document':
      if (outOfDate(state.data.document[location.id])) {
        dispatch(data.document.fetch(location.id))
      }
      return
    }
  }
}

renderer(state, dispatch)

This is the actor which every application has. It usually sits at the end of your actors sequence, rendering the result of the preceeding actors.

Your renderer can be implemented with anything: Vanilla JS, jQuery, riot.js, the list goes on. Personally, I use React:

var APP_NODE = document.getElementById('react-app')
function renderer(state, dispatch) {
  ReactDOM.render(
    <Application state={state} dispatch={dispatch} />,
    APP_NODE
  )
}

But what is this <Application /> component? Think of it as a nexus; it accepts the entire application state, then uses that state to decide which view to render.

Great, but how do I make this work with my router?

If you’re using react-router, then your <Router> component does most of the job of <Application> for you. The problem is that it doesn’t do the most important part; it doesn’t pass state or dispatch through to your route handlers.

One solution is to wrap your <Router> component with an <Application> component which passes state and dispatch to your route handlers via a React Context. But if that sounds a little complex, there is a simpler way: doing the routing yourself!

function Application(props) {
  const location = props.state.navigation.location

  switch (location.name) {
    case 'documentEdit':        
      return <DocumentContainer {...props} id={location.options.id} />
    case 'documentList':
      return <DocumentListContainer {...props} id={location.options.id} />

    default:
      return <div>Not Found</div>
  }
}

But how do you go about writing Container components now that you don’t need connect? How do you resolve routes and store them in state.navigation?

All will be revealed in my free guide to Simple Routing with Redux and React! Join my newsletter now to make sure you don’t miss out. And in return for your e-mail, you’ll immediately receive three print-optimised PDF cheatsheets – on React (see preview), ES6, and JavaScript promises. All for free!

I will send you useful articles, cheatsheets and code.

I won't send you useless inbox filler. No spam, ever.
Unsubscribe at any time.

One more thing – I love hearing your opinions, questions, and offers of money. If you have something to say, leave a comment or send me an e-mail at james@jamesknelson.com. I’m looking forward to hearing from you!

Read More

Related Projects

Share on Facebook0Tweet about this on TwitterShare on LinkedIn0Share on Reddit0

7 Comments Join The Dark Side Of The Flux: Responding to Actions with Actors

  1. Hadrian

    Hi James, nice post, sounds like a very interesting concept.

    I just had some trouble understanding: would actors work like function composition? ( The result of one actor’s action is passed to the next actor) or do they process in parallel?

    Reply
    1. James K Nelson

      Actors don’t really have a result, but they can modify the current application state by dispatching an action. And any dispatched actions are run before the next actor is run – so I guess you could say they’re run in series, not parallel.

      Reply
  2. Sebastien Lorber

    I don’t really like the term “actor” because it makes me think of the actor pattern used in Akka / Erlang.

    I agree with your pattern, mostly for coordination purpose. In the backend / CQRS / DDD / EventSourcing world, we use Sagas for this. I’ve written a little bit about that here and this pattern can be applied to React too.
    http://stackoverflow.com/a/33501899/82609

    I don’t really agree with the implementation detail, and think this coordinator should be able to manage its own state instead of calling store.getState(). A reducer could be an actor actually if that actor needs state to take a decision.

    One more thing: connect of redux is also done to solve potential performance issues when rendering from the very top. At Stample we have a production app that renders from the very top, and manage all state outside of React (even text inputs), and we start to see the performance limits of this approach (on inputs mostly) particularly on mobile devices that have a bad CPU.
    See also:
    https://www.youtube.com/watch?v=zxN8FYYBcrI
    http://stackoverflow.com/questions/25791034/om-but-in-javascript/25806145#25806145

    Reply
    1. James K Nelson

      Good points.

      Regarding the performance issues, do you use ImmutableJS (or something similar) and shouldComponentUpdate in your components, to ensure you’re only rendering Virtual DOM when your props have actually changed?

      Regarding reducers being able to be actors – this would undermine Redux’s stance on reducers being pure functions. Too many actors would certainly make code harder to reason about – not easier.

      Reply
      1. Sebastien Lorber

        Yes, all my state is outside of React and absolutly immutable (but not with ImmutableJS: it did not exist at this time). There are still little performance issues even if PureRenderMixin kicks in everywhere it can, mostly because when rendering from the VERY TOP, you always have to re-render the layout and it can become quite expensive on complex SPA apps.

        I understand the point of keeping reducers pure, but actually these “saga reducers” would never be used to drive react renderings but only drive complex transversal features (like an onboarding). The reducers that compute state to render should absolutly stay pure.

        With your “actor pattern”, you have side effects on events. Whether it comes from a reducer or not is just an implementation detail that does not matter that much. Just wanted to point out that it is not necessary to introduce new technology like RxJS (see SO post). What I don’t like in your implementation is that the actor has to understand too much of the structure of the state. And it uses the state that is computed for rendering purpose. I think the “actor” should be able to manage its own state instead of using UI state to take appropriate decisions.

        Reply
  3. Karol Selak

    There’s one potential problem in this solution, I think. What if we will receive two actions in a row? First action will block the subscription until all actors will be finished, so the second action won’t be detected at all. Am I wrong?

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *