blog

A problem with Redux: how to prevent the state from growing forever

In my last blog post I explained how we used Redux to organize the data flow in an application; however, Redux has a rare problem that doesn't seem to have a simple solution (with simple, I mean not having to install another 26 libraries): as we create new states, the old states get archived, and this can mean several megabytes of data stored in the client.

Now, there are good reasons why this should not be a problem for 99% of the applications:

  1. When we make a new state, we create a shallow copy of the previous one, not a deep copy. This means that the references will still point to the same data, except for the ones that changed. In other words, if your old state took 200kb and your new state created another 1kb, the total amount will be 201kb, and not 401kb.
  2. Most websites don't store that much data, so even if you use the same single-page app for days, you'll likely not even reach 1Mb

Despite being rare, it is a problem. So how can we solve it?

I'll explain it with an example: an application in which you can turn a lightbulb on and off, and also select its colour (red, green, and blue). It also has a little side effect: if the lamp is off and you change its colour, it turns on.

I will first make an application using only React + Redux, and then, I will use Flux (a paradigm similar to Redux, but that only stores one state instead of the whole archive) to solve the problem.

This is how we could build this application with React + Redux:

Observations:

I will make this application in a single file, so if you simply copy and paste the code below in order, it should work.

This is how my imports look like:

import React from 'react';
import ReactDom from 'react-dom';
import { Provider, connect } from 'react-redux';
import { createStore, combineReducers } from 'redux';

We should also try to imagine how the state would look like in order to plan our reducers. Since we need to store colour and power of the lightbulb, we could model our state this way:

// This is not actual code, but just a representation of how the state would look like
// You do not need this in the file
{
  isOn: false,
  colour: '#FF0000'
}

This means that we will have 2 reducers: isOn and colour.

#2 Making the actions to toggle the light on/off and also the colour

// Action types
const TOGGLE_LIGHT = 'TOGGLE_LIGHT';
const CHANGE_COLOUR = 'CHANGE_COLOUR';


// Actions
const actions = {

  // Receives nothing
  toggleLight() {
    return {
      type: TOGGLE_LIGHT,
    };
  },

  // Receives a value for the new colour
  changeColour(value) {
    return {
      type: CHANGE_COLOUR,
      payload: value,
    };
  },

};

#3 Now we create the reducers

// Reducers
const reducers = {

  // Reducer for the 'isOn' attribute
  isOn(state = false, action) {
    const type = action.type;

    switch(type) {
      case TOGGLE_LIGHT:
        return !state;
        break;

      // When the user changes a colour, we turn
      // on the lights
      case CHANGE_COLOUR:
        return true;
        break;

      default:
        return state;
        break;
    }
  },

  // Reducer for the 'colour' attribute. The default
  // colour will be red
  colour(state = '#FF0000', action) {
    const type = action.type;
    const payload = action.payload;

    switch(type) {
      case CHANGE_COLOUR:
        return payload;
        break;

      default:
        return state;
        break;
    }
  },

};

#4 Combine all the reducers into a root reducer

Now that we have all the reducers, we must put them together into a single one:

// The root reducer groups all the other reducers together
const rootReducer = combineReducers({
  isOn: reducers.isOn,
  colour: reducers.colour,
});

#5 Create the store

Now we create the store and pass the root reducer:

// Store
// The resulting state that we get from the reducers
// would look like this, if the light was turned on
// and the colour was green:
// { isOn: true, colour: '#00FF00' }
const store = createStore(rootReducer);

#6 Create the React component

In this case, I am using a shorthand for creating React components: it receives the props isOn, colour, toggle (function), and changeColour (function):

// React component for the lightbulb
const Lightbulb = ({
  isOn,
  colour,
  toggle,
  changeColour,
}) => (
  <div>
    {isOn ? (
      <span style={{ color: colour }}>ON</span>
    ) : (
      <span>OFF</span>
    )}
    <br />
    <button onClick={toggle}>Turn {isOn ? 'off' : 'on'}!</button>
    <button onClick={() => changeColour('#0000FF')}>Blue light</button>
    <button onClick={() => changeColour('#00FF00')}>Green light</button>
    <button onClick={() => changeColour('#FF0000')}>Red light</button>
  </div>
);

#7 Bind the React component to Redux

Here I am using the connect function provided by Redux to connect the component to our state and dispatcher:

// Element to be rendered (Lightbulb connected to Redux)
const LightbulbElement = (() => {

    const mapStateToProps = (state) => ({
    isOn: state.isOn,
    colour: state.colour,
  });

  const mapDispatchToProps = (dispatch) => ({
    toggle() {
      dispatch(actions.toggleLight());
    },

    changeColour(colour) {
      dispatch(actions.changeColour(colour));
    },
  });

  return connect(
    mapStateToProps,
    mapDispatchToProps,
  )(Lightbulb);

})();

#8 Make the Application component

The Application component will be the main component: we will use the Provider component from redux in order to bind the store:

// Application (the element with redux bound to the store)
const Application = (
  <Provider store={store}>
    <LightbulbElement />
  </Provider>
);

#9 Rendering

Now we can render the Application component in the dom:

// Rendering the app in the #app div
ReactDom.render(Application, document.getElementById("app"));

Done!

Ok, here is the problem: what if "colour" was actually a string of 20Mb? If you don't care about the old versions, you probably should not be archiving them. To solve this problem, we could implement our own separated store only for the colour; this store would be responsible for keeping only the newest version of the string and notify the components when it gets changed.

This is very similar to what Flux does (another pattern, like Redux), so I am going to use it in my solution. Ok, I know I said I did not want to "use 26 more libraries", and I do recommend you to build your own methods for it; in this case, however, I am going to use Flux and its libraries because 1- this is just a quick explanation, 2- it's fun, 3- I feel like doing it. Sorry.

Observations

Again, this application will be in a single file, so you can just copy and paste the code.

My imports:

import React from 'react';
import ReactDom from 'react-dom';
import { Provider, connect } from 'react-redux';
import { createStore, combineReducers } from 'redux';

// Two new imports:
import { Dispatcher } from 'flux';
import { EventEmitter } from 'events';

In this case, the state would be different: we no longer will be holding the colour, only the isOn attribute:

// This is not actual code, but just a representation of how the state would look like
// You do not need this in the file
{
  isOn: false
}

#1 Creating the Flux dispatcher

For Flux, we need to instantiate our own dispatcher:

// Flux dispatcher
const dispatcher = new Dispatcher();

#2 Making the actions to toggle the light on/off and also the colour

The actions are going to be almost identical, with one exception: the action for changing the colour will be returned and dispatched to Flux:

// Action types
const TOGGLE_LIGHT = 'TOGGLE_LIGHT';
const CHANGE_COLOUR = 'CHANGE_COLOUR';

// Actions
const actions = {

  toggleLight() {
    return {
      type: TOGGLE_LIGHT,
    };
  },

  changeColour(colour) {

    // Action to be returned and dispatched
    const act = {
      type: CHANGE_COLOUR,
      payload: colour,
    };

    // Flux dispatch
    dispatcher.dispatch(act);

    return act;
  },

};

#3 Now we create the reducers

Since we are not storing the colour in the Redux state anymore, we will only have one reducer: isOn. We will listen to the CHANGE_COLOUR action, but only to toggle the lights on if we change them - the new colour will be ignored.

// Reducers
const reducers = {

  // Reducer for the 'isOn' attribute
  isOn(state = false, action) {
    const type = action.type;

    switch(type) {
      case TOGGLE_LIGHT:
        return !state;
        break;

      // When the user changes a colour, we turn
      // the lights on
      case CHANGE_COLOUR:
        return true;
        break;

      default:
        return state;
        break;
    }
  },

};

#4 Creating the root reducer and the Redux store

These steps are almost the same, but now I only have one reducer:

// The root reducer groups all the other reducers together
const rootReducer = combineReducers({
  isOn: reducers.isOn,
});


// Redux Store
// The resulting state that we get from the reducers
// would look like this, if the light was turned on
// and the colour was green:
// { isOn: true }
const store = createStore(rootReducer);

#5 Creating the Flux store to hold the colour

This part is new: this is where we will store the colour of the lightbulb, also providing a method for components to listen to the store in case it changes (using an event emitter) and providing a method to set a new value.

// Flux store for the colour: the store can emit events, so we
// inherit methods from the EventEmitter
const colourStore = (() => {
  let cache = '#FF0000';

  return Object.assign({}, EventEmitter.prototype, {

    // Getters and setters
    getColour() { return cache; },
    _setColour(v) { cache = v; },

  });
})();


// Registering the Flux colour store in the dispatcher: when we
// dispatch an action, we'll check if it is of the right type, and
// then we'll set the colour in the store
dispatcher.register((action) => {
  switch(action.type) {

    case CHANGE_COLOUR:
      colourStore._setColour(action.payload);

      // When the store changes, we emit an event to notify
      // the components that are subscribed
      colourStore.emit('change');
      break;

  }
});

#6 Creating the React component

This React component will not be as simple as the previous one: it will have a state; the state will carry the colour of the lightbulb. When we create the component, we get the initial state from the store (colourStore.getColour()) and we will also subscribe to the store (celularStore.on('change', () => { ... })): when the store changes, we will get the new colour and set the new state (this.setState).

// React component for the lightbulb
class Lightbulb extends React.Component {

  constructor(props) {
    super(props);

    // Getting the initial state
    this.state = { colour:  colourStore.getColour() };

    // Listening for changes in the store: we update the
    // state whenever it changes
    colourStore.on('change', () => {
      this.setState({ colour: colourStore.getColour() });
    });
  }

  render() {

    // We are not getting the colour from the props anymore
    const {
      isOn,
      toggle,
      changeColour,
    } = this.props;

    return (
      <div>
        {isOn ? (
          <span style={{ color: this.state.colour }}>ON</span>
        ) : (
          <span>OFF</span>
        )}
        <br />
        <button onClick={toggle}>Turn {isOn ? 'off' : 'on'}!</button>
        <button onClick={() => changeColour('#0000FF')}>Blue light</button>
        <button onClick={() => changeColour('#00FF00')}>Green light</button>
        <button onClick={() => changeColour('#FF0000')}>Red light</button>
      </div>
    );
  }
}

#7 Binding the React component to Redux, making the Application component, and Rendering

Everything is the same now, except that we are not passing the colour as a prop anymore:

// Element to be rendered (Lightbulb connected to Redux)
const LightbulbElement = (() => {

    const mapStateToProps = (state) => ({
    isOn: state.isOn,
  });

  const mapDispatchToProps = (dispatch) => ({
    toggle() {
      dispatch(actions.toggleLight());
    },

    changeColour(colour) {
      dispatch(actions.changeColour(colour));
    },
  });

  return connect(
    mapStateToProps,
    mapDispatchToProps,
  )(Lightbulb);

})();


// Application (the element with redux bound to the store)
const Application = (
  <Provider store={store}>
    <LightbulbElement />
  </Provider>
);


// Rendering the app in the #app div
ReactDom.render(Application, document.getElementById("app"));

Done! Redux will keep a history of the isOn property of the lightbulb, but the colour will not be archived.