Managing the state with Redux

As explained in the previous part, we chose to keep track of application state using Flux, and especially one of its most common implementations, Redux.

This article describes design patterns we found useful while working with Redux. Note that we also discuss about Redux patterns in the part dedicated to Flow patterns, because we tried to statically type our actions and our interactions with the back-end as often as possible.

Designing reducers

This part is deeply linked with what was discussed earlier about application structure, because we tended to choose the same kind of structure for our state store and for our folders. For example, we tried to put the reducer related to a root directory of src/pages at the root of our state store, while sometimes using combineReducers to nest several reducers under a single store key when needed. However, we tried to limit this to only one level of depth, because understanding a complex state store tree structure is quite difficult.

Preserving state immutability

As a lot of teams, we did not made the things more complex than they need to be (and reduced runtime overhead), and simply used the ES7 object spread operator to recreate the state without mutating it. To read more about why we made this choice, and get an insight on other alternatives, read our article on immutability.

Switches or maps ?

Another tradeoff came before us : write reducers as a big function using switch, or write them as maps whose keys are the types of actions that may be dispatched. We know both approaches have their upsides and their downsides, but we made a definite choice for a simple reason : Flow does not looks on the inside of maps, except if you force it to do so by creating an object type with the right keys. We did not want to have to rewrite a list of all actions handled by our reducer outside of it, so we stuck with the switch option.

Allowing ourselves to change our minds

To ease refactoring, we define a getLocalState method inside of each reducer's file. To this method, we can pass the global application state as a parameter, and it gives us back the part of state that is managed by this specific reducer. This allows us to keep the information about where this reducer is mounted in the state tree in a single place, so we are safer when moving it and refactoring the app.

// fooReducer.js
export const getLocalState = (globalState: Object): State => {
  return globalState.someSpecificPart.foo;
}

Side note : as you may have noticed, this code snippet has Flow type annotations. This is the second benefit of this pattern. Read more about this in the article about Flow.

I don't want to store carrots in my socks drawer

As you may have already noticed if you have worked with Redux, when having quite a big number of reducers to work with, you often find yourself wondering how you went up to having so much data, often of different kind, sometimes duplicated, stored in your store. This sometimes involves information about the user interface mixed with pure business data, or business data that's needed in two pages of your app, hence duplicated in two places of your state store.

Several options exist to avoid this. As we said earlier, we wish we had those reflexions earlier in the development of our app, because it made reasoning about our store quite hard.

Splitting pure data and interface state

This involves dedicating a part of your state store to pure data, and another part to interface state. Here's an example store structure :

{
  data: {
    products: {
      fruits: {
        oranges: [ /* ... */ ],
        apples: [ /* ... */ ],
      },
      vegetables: {
        potatoes: [ /* ... */ ],
        carrots: [ /* ... */ ],
      },
    },
  },
  interface: {
    menu: { collapsed: true }},
    activeFilters: {
      searchBar: 'appl',
    },
  }
}

Upside : this approach is very close to the way a local database would work. This enables to understand easily the underlying data model, and to decouple actions and reducers from the internals of your presentational components.

Downside : You need to interact with many actions and many parts of your state store when you need to dispatch changes. It might be harder to track down what happens in your reducers, because some actions may tend to have strange side effects when you want them to interact with a remote part of the state store.

  1. Splitting your state store by business concerns :
    {
    category: {
     name: 'fruits',
     description: 'Yummy fruits !'
     products: {
       oranges: [ /* ... */ ],
       apples: [ /* ... */ ],
     }
     activeFilters: {
       searchBar: 'appl',
       ...
     },
     ...
    },
    menu: { collapsed: true }},
    }
    

Upsides :

  • easier to reason about when the split between pages is clear in your app (say we have a "category" page which displays the list of products in the given category)
  • nesting reducers allows flushing a whole part of the state (when we change category, we discard this whole store key and don't keep any unwanted data)

Downside : when a piece of data is transversal (such as the menu here), you have to create a smaller top-level store part just to keep track of it. This can lead to a state store growing exponentially, and being difficult to reason about.

Don't put everything in the store

One of our refactoring processes that helped us have a clearer view of our state store was to remove a lot of data from them, because we realised they didn't need to be there in the first place. For example, we had a page that was used to edit data, with a popup displayed when leaving it asking you to either save or discard your changes. This means the data related to the edits that had been made needed to be available from only one component, and only existed between its mounting and its unmounting. For this purpose, no need to use the Redux store ! So we modified our component to make use of its internal state instead of the global store, and saved lines of codes, dispatched less actions and made our state store easier to reason about (no need for fooData and fooDataBeingEdited anymore).

Don't make your actions too smart

When dispatching actions, most of the time, you'll want them to have side effects. However, we found useful to avoid having too much things performed in background by a single action. To serve this purpose and help make the background computations easier to track down, we used Redux Thunk middleware. Its behaviour is quite simple : instead of creating static actions, you write "action creator" functions. When Redux Thunk sees that you've passed dispatch a function, it calls it, passing it dispatch and getState as arguments. One common implementation pattern we found useful, for actions that abstract fetching data from the back-end, was the following :

export function fetchCarrotsAction() {
  return async (dispatch, getState) => {
    dispatch({ type: FETCH_CARROTS });

    try {
      const carrots = await getCarrotsService();
      dispatch({
        type: RECEIVE_CARROTS,
        carrots
      });
    } catch(err) {
      // handle the error
    }
  }
}

This pattern allows us to have actions that have side effects, but still expose a clear reporting about the actions, and keep them simple (one side-effect per dispatched action).

When should a component be connected ?

Once again, you may often have a hard time deciding whether a component should be connected to the state store, or receive its input data and event handlers from its parent. This depends on several things, but here are the upsides and downsides of each method, to help you make the trade-off :

* Connecting everything down the components tree makes refactorings easier, because you don't have to
    wonder whether a sub-sub-sub-subcomponent is being given the right data. However, it may make your app
    harder to reason about, and make a mess of your state store by needing a lot of keys in your store, and dispatching
    actions everytime something happens ;
* Connecting only top components and passing the data and handlers to theirs children in the JSX may seem easier or
    cleaner at the beginning, but as your component become more complex, you'll often find yourself writing
    a lot of props and handlers in your JSX, and not even needing them in the component you're passing them to,
    as it's simply passing them transparently to one of its children.

We often found a sweet spot betwwen those two extreme options, which we also sometimes decided to change (read more about this above in this article). This was quite a hassle, and, on this point specifically, we advise you to weigh your choices wisely before implementing them.

What about the "container" pattern ? Some of us used it heavily (probably overusing it a bit, sometimes), some others didn't felt the need to use it. Maybe we should have explored it more in-depth. Do you have advices or ideas on this part ? Feel free to tell us in the comments section below !

results matching ""

    No results matching ""