Michal Zalecki
Michal Zalecki
software development, testing, JavaScript,
TypeScript, Node.js, React, and other stuff

Reduce side effects in React/Redux

Pure functions are functions which for certain input always returns the same output without modifying its surroundings. So, they are free from side effects. Because of that feature they are easy to test and highly reliable part of your system. Why only a part? There is a lot of different side effects and it’s more probably than not that your app is full of them. Every DOM mutation, API request, pushState or even console.log is a side effect. It’s hard to imagine an useful application without side effects.

Cycle style

The closes try to get rid of side effects from your code are drivers from Cycle.js. Cycle apps are build around the idea of passing observables a.k.a. streams. Sinks (driver’s input) passed from pure components to drivers and sources (driver's output) to components input. By the way, Cycle is so smart name, isn’t it? In Cycle.js drivers the part of the app in which all side effects happen. Thanks to growing community you don’t have to write drivers by yourself. Let’s keep in mind for now the idea of having side effects in one place.

Flux apps, Redux apps especially, are build around the idea of store(s) and one‑way data flow. In the beautiful charts showing how flux works there was not enough room for most of side effects. API calls in action creators and DOM modification in components, but what about the rest of them? It was sure that people will try to put side effects more or less randomly as everything felt like a “right” place and I was guilty of such approach too.

Components

Projects on which I was working were growing and I decided to standardize my approach to side effects. Some React community authorities proposed to deal with side effects inside components: gaearon/react-side-effect. At that time it sound reasonable. All in all my app is built from small blocks called components so why not!

Everything went great until new screen had to be done in the project. You are able to start using our service before signing up. Everybody like to try core feature without giving their email and other personal details! In the middle of the process we need to store some data so you need an account. The screen had to have two forms. One for signup and one for login. I had login form and I had signup form so it seemed to be a trivial task. The problem was each of those form is making redirect to a different URL and the flow was to redirect you to the third one. First though? Props! Let’s pass the URL to redirect as a component property! I know it’s a hack, code smell or at least hotfix, but it was working and it was still relatively easy to test.

Someday flow has changed again and we should make a redirect also based on the set of previous actions. Very logical and intuitive from the user’s point of view but at the end spaghetti code inside the component. I’ve started with one liner and ended up with function which glue together some strings in the hope for valid URL. Lesson learned. Side effects in components didn’t work, at least for me.

Actions

I’d like to make some side effects in actions which already were making API calls (which as I mentioned is also a side effect). I gave up the idea quickly when it come to testing. I had pure or almost pure actions which become testing playground for mocking everything. It’s always better when your actions are idempotent. Side effects in actions didn’t work, at least for me.

Stores

Side effects and stores (using alt at that time)? Store is suppose to be a container for state. No one would like to have state influenced directly by side effects. I know that and in the same time I was running out of options. I even though that MV* which I was doing using Angular wasn’t so bad (it was just temporarily weakness, I'm over it). I took a walk and get back quickly as I come up with the idea of stores only for side effects. I’ve rewritten our toast notifications caled alerts. AlertsStore was great! Implementing toasts which will show after some period or redirect was a piece of cake! Side effects in stores (almost) did work, at least for me.

If not dispatcher and this awful "feature" I wouldn’t have to refactor this even once!

Redux

Stores have this major downside of managing data dependencies between stores and waitFor which name is a lie and actually waitFor isn't waiting for anything. Docs were like gibberish and I couldn’t understand how it’s working before I read the source code. I was astonished when I discovered that dispatch was actually synchronous.

Lesson learned. I was writing my side effects inside reducers. Separate reducers dedicated for effects. I had everything I needed: state and current action. Returning unmodified state each time was weird but I’ve made a helper/wrapper to prevent accidental state change and didn’t bother anymore. Side effects in reducers did work, battle-tested.

Middleware and room for improvement

There were at least two things I would like to change. First was that I didn’t like the need to return state. Second the name reducer for effects was misleading. I would like to also be sure that my effects happens after the last reducer is invoked and object’s keys (combineReducers) cannot guarantee that.

The mechanics and order of enumerating the properties ... is not specified.
― Standard ECMA-262

Redux supports middleware which is great for extending Redux capabilities.

// src/effects/middleware/createEffects.js

export default function createEffects(effects) {
  return store => next => action => {
    next(action);
    effects.forEach(effect => effect(store.getState(), action, store.dispatch));
  };
}

Then you can pass an array of middleware and apply it to Redux store.

// src/store.js

import { createStore, combineReducers, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import createEffects from "src/effects/middleware/createEffects";

const rootReducer = combineReducers({
  ...
});

const effectsMiddleware = createEffects([
  ...
]);

const store = createStore(rootReducer, compose(
  applyMiddleware(thunk, effectsMiddleware),
  window.devToolsExtension ? window.devToolsExtension() : f => f,
));

export default store;

This implementation fulfills my needs:

  1. I don’t have to return state
  2. effects name is straightforward
  3. I can be sure my state won’t be changed later by the same action
// src/effects/sessionEffects.js

/* eslint default-case: 0 */

import { browserHistory } from "react-router";

import {
  LOGIN_SUCCESS,
  LOGIN_FAILURE,
  LOGOUT,
} from "src/constants";

export default function sessionEffects(_state, action) {
  switch (action.type) {
    case LOGIN_SUCCESS:
      localStorage.setItem("token", action.payload.data.token);
      browserHistory.push("/account");
      break;
    case LOGIN_FAILURE:
      localStorage.removeItem("token");
      break;
    case LOGOUT:
      localStorage.removeItem("token");
      browserHistory.push("/");
      break;
  }
}

Redux Saga

Sagas are different approach for dealing with side effects. Redux Saga is side effects middleware build with using generators in mind. The idea itself isn't new. When using sagas you are suppose to perform side effects inside generators which replaced asynchronous actions (e.g. redux-thunk). Side effects in “actions”? I consider sagas as 3 steps forward and 1 steps backward. Async generators or await/async work great as actions and you don’t need sagas to do that. Redux Saga shines in side effects composition and with the ease of testing. Using redux-store-mock is not fun for sure (still great it exists!) while testing sagas is fun. The only problem is that some of those tests base on mocking. Operators can behave similar to stubs/spies when testing.

Summary

The best part is that I don’t have to give up the idea of reducing side effects when using sagas. In the next two or three weeks I’m going to start next project which’ll be build with Redux Saga so maybe I’ll have some interesting thoughts to share. Stay tuned!

Hero image by Patrick Tomasso on Unsplash.