Good evening, Wolfpack! This session is building on top of the React session that precedes it, so if you need a refresher, check it out! Our goal today is to gain an understanding of React-Redux, and encapsulating complex state management flows with the effects system of Redux-Saga.

State? I thought we managed state in Components?

Component state should be considered something specific to the component itself, generally. As an example, consider an accordion component which manages its open/closed status with a state property. When managing the internal state of a component, keep it in the component. When managing a state that crosses multiple component boundaries, this is where Redux shines.

Redux

Redux is a very simple library that makes it easy to separate the logic that manages state from the things that initiate those changes. In pure Redux terms, the logic that compresses state changes to the latest state object is called a reducer, and the things that initiate those changes are called actions. The overall state management object which handles dispatched actions and reduces the state is called the store. When combined with React via react-redux, you get a way to manage state from anywhere in the app with some convenience methods that take away some of the burdens of tapping into a global state tree.

Actions

Actions are simply objects with a type key. They may carry additional data to parameterize the action, but the core requirement of an action is that the type key is present. Typical conventions utilize upper-case string constants, but this is not required. Action creators, thus, are simple functions that return an action:

export const actionCreators = {
    increment: () => ({ type: 'INCREMENT_COUNT' } as IncrementCountAction),
    decrement: () => ({ type: 'DECREMENT_COUNT' } as DecrementCountAction)
};

These are handy for two reasons:

  1. They make construction of actions well-defined rather than expecting interactors with the store to dispatch arbitrary objects (which could break if the reducer’s expectations change).
  2. Action creators are readily bound to the dispatch method with bindActionCreator (this is called when you connect the store to a component with React-Redux).

Reducers

Reducers are also simple: they consume the previous state, and return an applicative change to that state, as a new state object:

export const reducer: Reducer<CounterState> = 
    (state: CounterState | undefined, incomingAction: Action): CounterState => {
    if (state === undefined) {
        return { count: 0 };
    }

    const action = incomingAction as KnownAction;
    switch (action.type) {
        case 'INCREMENT_COUNT':
            return { count: state.count + 1 };
        case 'DECREMENT_COUNT':
            return { count: state.count - 1 };
        default:
            return state;
    }
};

You can see that this does not alter the previous state object directly, but instead returns a copy if changed. This is a crucial behavior that is important to maintain. Keeping the reducer idempotent (when calling the function, it does not change in behavior when called multiple times) helps prevent bugs, makes the reducer easier to comprehensively test, and is an assumption expected by enhancements in Redux like the devtools so that you can utilize its “time travel” feature.

React-Redux

Adding React into the mix is easy. With the store configured and created, simply provide it to the <Provider store={store}/> component from the react-redux package, wrapping this component around your entire React app:

ReactDOM.render(
    <Provider store={store}>
        <ConnectedRouter history={history}>
            <App />
        </ConnectedRouter>
    </Provider>,
    document.getElementById('root'));

In order to expose a given action creator (or set of action creators) to a component, you only need export your component with the connect wrapper function:

export default connect(
    (state: ApplicationState) => state.counter,
    CounterStore.actionCreators
)(Counter);

The first argument to connect is the mapStateToProps argument. It expects a function which when given the root state object, returns an object whose keys will become properties on the component. The second argument is mapDispatchToProps. It expects a variety of options, which inevitably are either dispatch calls, or become dispatch calls (like action creators which can be bound to dispatch with bindActionCreator). This method returns a higher-level component (or component enhancer) which wraps your component and provides the store and actions as props. When using TypeScript, note you will need to update the type signature of your component to allow these as part of the props type parameter:

type CounterProps =
    CounterStore.CounterState &
    typeof CounterStore.actionCreators &
    RouteComponentProps<{}>;

class Counter extends React.PureComponent<CounterProps> {
    // ...
}

Next Steps

So far, so good, right? You no longer have to pass down functions from a grandparent component to make it possible to call APIs and refresh state – it’s so nice! But what about promises? You have to thread through the dispatch method and call it inside your .then callback. Ok, not great, but it still works. But what if your API is variable in response rate? What if your connection is flaky? What if a user just clicks the everloving hell out of a button that dispatches an action to call your API? There’s many ways to handle this. Loads of React apps will employ some kind of state machine management that alters how action creators function (losing functional purity or getting really messy), or put this logic in the reducer, or some other kind of workaround. These work, but then it starts to become a common pattern as it’s not a unique problem in the app, and suddenly your meta state management becomes a sort of transaction scoping handler. Thankfully this is a well understood (and solved) problem, and also thankfully, it has been solved for Redux. Enter redux-saga.

Redux-Saga

It is a major frustration of mine that there are very few solid tutorials out there for Redux-Saga, because the library is very simple in design, but difficult to understand how to apply it. How do I know it’s difficult to understand how to apply it? Because it’s been out for quite a while, and one of the simplest problems emerging from plain Redux (or redux-thunk) usage has roughly six times the results on Google compared to Redux-Saga itself: redux cancel action: 12M results

vs

redux saga: 2M results

Oof. And while you could ascribe it to marketing, there’s definitely a lot of confusion when you look up tutorials, many of which echo the basic example from Redux-Saga’s website with no real nuance to why someone would want to use it, or even how to choose the various effects. So let’s talk about what makes it tick!

In the above scenario of a long-running API call and an impatient user, assume the naive implementation of an action creator with redux-thunk middleware (the recommendation from the official Redux docs):

export const actionCreators = {
    fetchWeatherForecast: () => dispatch => {
        dispatch({type: 'REQUESTING_WEATHER_FORECASTS'});
        fetch(`https://someservice/v1/api/weatherforecast`)
            .then(response => response.json())
            .then(forecast => dispatch({type: 'RECEIVE_WEATHER_FORECASTS', forecast}));
    }
};

You could optionally use async/await syntax to reduce callback hell, but the main issue remains present regardless. If you want to block repeat calls until the first call has completed, you have to incorporate state:

export const actionCreators = {
    fetchWeatherForecast: () => (dispatch, getState) => {
        const state = getState();
        if (!state.weatherForecast.isLoading) {
            dispatch({type: 'REQUESTING_WEATHER_FORECASTS'});
            fetch(`https://someservice/v1/api/weatherforecast`)
                .then(response => response.json())
                .then(forecast => dispatch({type: 'RECEIVE_WEATHER_FORECASTS', forecast}));
        }
    }
};

Of course, this now requires your reducer is setting isLoading appropriately based on start/completion of the action. This is the easy scenario, all is still right with the world, no extra libraries are needed to manage state. BUT! What if you cared about getting the response for the last time the action was dispatched, rather than the first? I’ll leave that as an exercise for the reader in implementing with redux-thunk alone, and in the meantime, show how Sagas can help.

How Sagas bring storytelling to actions

It starts with a handy recent addition (relatively speaking) to the Javascript language: generator functions. How do they work?

function* foo() {
    yield "this";
    yield "then this";
    yield "also this";
}

let bar = foo();
bar.next() // { value: "this", done: false }
bar.next() // { value: "then this", done: false }
bar.next() // { value: "also this", done: false }
bar.next() // { value: undefined, done: true }

Redux-Saga uses generator functions to wrap complex workflows with very simple calls that declare the sequence of events and expectations. To add Sagas to redux, just import createSagaMiddleware() and take the collection of sagas and pass them into the middleware’s run() method:

export default function configureStore(history: History, initialState?: ApplicationState) {
    const sagaMiddleware = createSagaMiddleware();
    const middleware = [
        sagaMiddleware,
        routerMiddleware(history)
    ];

    const rootReducer = combineReducers({
        ...reducers,
        router: connectRouter(history)
    });

    const enhancers = [];

    const store = createStore(
        rootReducer,
        initialState,
        compose(applyMiddleware(...middleware), ...enhancers)
    );

    sagaMiddleware.run(rootSaga);

    return store;
}

Your root saga will look like this (very much like the root reducer):

export const rootSaga = function*() {
    yield all([
        WeatherForecasts.weatherSagas
    ]);
};

And your individual section of the sagas:

export const weatherSagas = [
    function*() {
        yield takeLeading('REQUEST_WEATHER_FORECASTS', handleWeatherRequest)
    }
]

In that takeLeading method, there is magic happening. What this does is all the logic we described above, in tracking the completion state of handling the action 'REQUEST_WEATHER_FORECASTS', and essentially discards the subsequent dispatches until the the provided generator function handleWeatherRequest has completed. Now what does that method look like?

function* handleWeatherRequest(request: RequestWeatherForecastsAction) {
    let forecast: Body = yield call(fetch, `https://someservice/v1/api/weatherforecast`);
    let data: WeatherForecast = yield call(() => forecast.json());
    yield put({type: 'RECEIVE_WEATHER_FORECASTS', forecasts: data });
}

Ok, so this looks very different from before. What are call and put? They are part of the collection of methods that Redux-Saga provides to manage the relationships between dependent action chains, and even enable awesome features like cancellable actions with fork and cancel. This decomposition allows for much cleaner workflows. Back to that question that prompted Sagas in the first place: what about handling the last request rather than the first? Instead of takeLeading, use takeLatest. Need to pause a saga until another action has occurred? wait for it with take in your series of yields. Need to buffer actions to be processed one at a time? Saga can do that with actionChannel.

When thinking about building a UI for a chat client, this may start to ring a bell: we want users to have a responsive UI that lets them send messages as fast as they can type them, but each one needs to be processed in order. Using actionChannel buffers for individual rooms (or in Howler’s terms, Channels, but I want to avoid the ambiguity here), you can allow a user to (from their point of view), send many messages in one room, switch to another, send more messages, and if their connection is slow, or flaky, or Howler’s API itself has crumbled under their rapid-typing might, the ordering of messages delivered as actions are preserved relative to the room, giving a great user experience. You could do that with redux-thunk, but I don’t even want to begin thunking about how you’d do it well.

Cassandra Heart

Cassie Heart is the creator of Code Wolfpack, BDFL of Howler, The Bit with a Byte, Resident Insomniac.