blog

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

In my [last blog post](http://hcoelho.com/blog/21) 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.