blog

Organizing data flow with React + Redux

One part of our application had its frontend made with React, taking advantage of its reactivity to state changes, which is very helpful when you are building modern and responsive applications. However, we underestimated the complexity of this system, and maintaining it with React only became very complicated and tiresome; this is when we decided to adopt one more paradigm: Redux.

This will not be a tutorial, instead, I will only want to present a general idea of how all these tools work.

I will first make a quick introduction on how React works: the easiest way to understand it, for me, is by imagining it as a way to make custom HTML elements. For example, say you have the following pattern:

<div>
  <h1>Header</h1>
  <p>Body text goes here<p>
</div>

Wouldn't it be nice if instead of typing all these divs, h1s and ps, you were able to make a custom element with that format (maybe call it Section)? With React, it would be easy:

class Section extends React.Component {

  render() {
    return (
      <div>
         <h1>{this.props.title}</h1>
        <p>{this.props.children}<p>
      </div>
    );
  }

}

Props are parameters passed to the component (like an html attribute or children), they are recovered from the this.props object.

Now to render this element with React:

<Section title="Header">
    Body text goes here
</Section>

React also has the concept of State, which refers to the mutable state of a component. For example: a lightbulb of 60W would have "60W" as a prop, but whether it is on or off, it will depend on its state.

States are very easy to work with, we set the initial state in the constructor, and every time we need to modify it, we use the method this.setState to pass the new state. The component will update itself automatically.

class Lightbulb extends React.Component {

  constructor(props) {
    super(props);
    this.state = { isOn: false };
  }

  toggle = () => {
    this.setState({
      isOn: !this.state.isOn,
    });
  }

  render() {
    let message;
    if (this.state.isOn) {
      message = 'On!';
    }
    else {
      message = 'Off!';
    }

    return (
      <div>
        {message}
        <button onClick={this.toggle}>Click me!</button>
      </div>
    );
  }

}

But things start to get complicated when our application grows: sometimes we need to access the state of a component from another component, sometimes they need to be shared: for this, we have to remove the state from the component and pass it to its parent, so the component only receive its values as props.

The tendency, therefore, is that all the state will end up in the root component, and all the child components will only receive props: all the state lives in the root component, which are passed down the tree as props; similarly, whenever an event happen on the bottom of the tree, it will bubble up to the top.

This is when better paradigms start to appear: the most popular used to be Flux, and now, it is Redux.

Redux is more a paradigm than a library - you don't need to use the library, but they do provide you some boilerplate code. It also respects this tendency of all the state living in a single root, which is called the store: the store is an object that contains the state of the whole application; and this is an important detail: you do not modify the state that lives in the store, you create a new "version" of this state - the old states get archived - this makes logging and debugging extremely easy. When you use the store provided by the Redux library, it will take care of recording the old states for you.

Myself, I would abstract the data flow of React + Redux in 5 simple steps:

A component triggers an action (example: a button is clicked) The action is sent to the reducer (example: turn on the light) The reducer creates a new version of the state, based on the action (example: { lightsOn: true }) The store gets updated with the new state The component gets re-rendered based on the new state

1 A component triggers an action

To make the component trigger an action, we simply pass the function (action) as a prop - the component will then call it whenever the right event happens:

// In the lines below, we are binding the state from the store,
// as well as a function that dispatches the action to toggle
// the lights on/off. The "dispatch" function is provided
// by the Redux library - we only need to make the "toggleLight"
// action ourselves

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

const mapDispatchAsProps = (dispatch) => ({
  toggle: () => {
    dispatch(actions.toggleLight());
  }
});

// The 'connect' function is also provided by the Redux
// library, it binds the props and methods to the React
// component
const LightbulbElement = connect(
    mapStateAsProps,
    mapDispatchAsProps,
)(Lightbulb);


class Lightbulb extends React.Component {

  render() {
    let message;
    if (this.props.isOn) {
      message = 'On!';
    }
    else {
      message = 'Off!';
    }

    return (
      <div>
        {message}
        <button onClick={this.props.toggle}>Click me!</button>
      </div>
    );
  }

}

And to render this element:

<LightbulbElement />

2 The action is sent to the reducer

An action is sent to all reducers automatically every time we use the dispatch method I described above. But what does that toggleLight look like? Like this:

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

Actions usually return objects with 1 or 2 parameters: type and payload. The type parameter refers to what kind of action you are performing: every action should have a distinct type. The payload parameter contains additional information that you need to pass to the reducer.

3 The reducer creates a new version of the state, based on the action

Reducers are responsible for replacing the current state of the application with a new one. For every attribute in the state (for example, say our state object contains the attributes "isOn" and "colour"), we should have a distinct reducer - this will ensure that one reducer will not modify an attribute that does not belong to it.

In our case, since we only have one attribute (isOn), we would create only one reducer; it would check the action type to make sure that piece of the state should be changed, if it should, it must create a new version of the state and return it:

// This function receives "state", which is the previous state in our store,
// and "action", which is the action dispatched
function isOnReducer(state = false, action) {
  switch(action.type) {

    case 'TOGGLE_LIGHT':
      return !state;
      break;

    default:
      return state;
      break;

  }
}

In other scenario, say we are receiving a payload and we are going to modify a state that is an object:

function myOtherReducer(state = { colour: 'black', opacity: 1.0 }, action) {
  switch(action.type) {

    case 'CHANGE_COLOUR':
      // Notice that I am using the spread operator (...) to create a new object
      // and recover the values of the previous state; then overriding the colour
      // with what I received from the payload
      return { ...state, colour: action.payload };
      break;

    case 'CHANGE_OPACITY':
      return { ...state, opacity: action.payload };
      break;

    default:
      return state;
      break;

  }
}

4 The store gets updated with the new state

This part is done automatically by Redux, we only need to give it our reducer:

import { createStore } from 'redux';

import { isOn } from './reducers';

const store = createStore(isOn);

export default store;

5 The component gets re-rendered based on the new state

This is also done automatically. Redux will detect if the parts of the store that a component uses changed - if it did, the component will get re-rendered.