AI, startup hacks, and engineering miracles from your friends at Faraday

Using context in redux-saga

Nick Husher on

At Faraday, we use redux-saga in our client-side webapp because it offers a robust and testable set of tools for managing complex and asynchronous effects within Redux apps. I recently upgraded our version of redux-saga from an embarrassingly old version to the latest, in part to take advantage of a new (but largely undocumented) feature: context.

The problem

I want to be able to write sagas without having to implicitly import any statically-declared singletons. The reason for that is simple: if I'm testing the saga that creates new users, I don't want to drag along the API layer or the router instance.

There are also a few modules in the codebase that only work in a browser and will fail loudly when I try to run them in mocha. If one of these browser-only modules ends up in an import chain, suddenly my tests start failing for bad reasons. At worst, I have to rethink my testing strategy to include a mock browser environment. I'm lazy and hate testing, and I think mocks are a gross awful code smell that should be avoided at all costs.

The solution

One of redux-saga's greatest features is that I can test a saga's behavior without actually executing that behavior, or even really knowing much about the details of how that behavior alters the world. It it should be possible to apply that to dependencies as well. And it is possible in redux-saga 0.15.0 and later using context.

Right now, there are very limited docs for context, but acts as shared value across all sagas that can be read with the getContext effect and written to by the setContext effect. It can also be set when the saga middleware is created by passing a context object as configuration.

Example

Let's say we're writing an app that fetches game inventory data from the server using some kind of authentication. We don't want tokens and authentication to bleed into all our sagas, so we wrap it up in a nice singleton API service:

class ApiService {
  getInventory = () => {
    return fetch('/api/inventory', {
      headers: {
        Authorization: `Bearer ${this.token}`
      }
    }).then(res => res.json())
  }
}

const api = new ApiService()
api.token = localStorage.token

export default api

Without context, our saga would probably statically import the API singleton:

import { call } from 'redux-saga/effects'
import api from './api' 

export function * fetchInventorySaga () {
  const inventory = yield call(api.getInventory)
  // Do something with the inventory data...
}

All is well until we try to test it in nodejs/mocha:

import { fetchInventorySaga } from '../src/sagas/inventorySaga.js
// ReferenceError: localStorage is not defined

There is no localStorage in the nodejs global context. We can either pull in a testing harness to change how import api from './api' is resolved, attempt to run the tests in a browser, or roll our own late-binding mechanism so that you don't need to import API and can pass the API instance in at runtime.

We need to solve the same problem for fetch, because that's also absent in nodejs.

Or, we could use context. Our saga changes only a little bit:

import { call, getContext } from 'redux-saga/effects'
// No more API import:
// import api from './api' 

export function * fetchInventorySaga () {
  // Get the api value out of the shared context:
  const api = yield getContext('api')

  const inventory = yield call(api.getInventory)
  // Do something with the inventory data...
}

We can now test the getContext effect just like we would any other redux-saga effect, and we can insert a mock value into fetchInventorySaga at test-time if we need to.

Setting up context in the main application is very straightforward. When you're creating your saga middleware:

import createSagaMiddleware from 'redux-saga'
import api from './api'

const saga = createSagaMiddleware({
  context: {
    api // our singleton API value
  }
})

Being able to late-bind singleton values like this has been enormously helpful writing robust tests in a complex codebase. I'll be steadily migrating the application code to use getContext more frequently, now that I have it as an option.

Confirm dialogs in Redux and React

Nick Husher on

a confirmation box

A fairly common problem in UI design is adding a modal dialog that confirms or cancels a dangerous action. Dan Abramov has a great StackOverflow answer on how to solve this problem for specific kinds of modal dialogs, but not for a generic, reusable modal that can serve many purposes without knowing what those purposes are. In this case, a confirm dialog.

A confirm dialog is made up of at least three parts:

  • Some help text for the user that describes what they are about to do and what any consequences of that action might be.
  • A choice that the user can make that confirms their action
  • A choice that bails out of that action and does nothing

These three parts are mirrored in the Redux action that the Faraday UI dispatches to show a confirm dialog:

showConfirmModal = (body, accept, cancel) => ({
  type: SHOW_CONFIRM_MODAL,
  payload: { body, accept, cancel }
});

The app's reducer sees this action and stores the new modal state in the redux store; the React UI tree then updates to show the modal dialog. This roughly matches the StackOverflow post linked above.

The big difference here is that the SHOW_CONFIRM_MODAL action takes two Redux actions itself, accept and cancel. When the user makes a choice, that action is dispatched to the app state. This has a two useful implications:

  1. A single confirm dialog can serve many possible purposes: instead of needing to build a new confirm dialog for each case where we might want one, we can reuse the same code for different purposes.

  2. We can easily attach confirm dialogs to any action in the system without building any new special code, or "leaking" the confirm dialog concern throughout our application logic.

Let's look at a concrete example where the confirm dialog is used: removing users from an account. Ideally, we want to write our system logic in a way that's ignorant of the confirm dialog, and we want to write our UI in a way that knows nothing about the details of deletion.

The naïve approach here is to post a KICK_USER action and not build a confirm dialog. Or, of you do build a confirm dialog, to embed the details in a thunk, saga, or whatever other tools you're using to manage asynchrony. This makes the action creator logic very simple, but potentially adds a bunch of complexity elsewhere:

// TODO: Handle confirm dialog
kickUser = user => ({ type: KICK_USER, payload: user.id });

Using the confirm dialog action creator described above, it becomes trivial to add safety to this destructive action. The action creator for removing users from an account ends up looking like this:

// Show a confirm dialog before deleting the user, natch
confirmKickUser => user => {
  let body = (
      <p>
        Are you sure you want to remove <br />
        <strong>{user.name}</strong> from this account?
      </p>);
  return showConfirmModal(body, kickUser(user));
};

Just like kickUser described above, we can easily wrap any Flux-style action creator in showConfirmModal and add a safety gate around it.

I hope this has been useful to you, we are certainly going to get a lot of mileage out of it in the Faraday UI codebase.