Redux Lifecycle

August 04, 2018 0 Comments

Redux Lifecycle

 

 

Disclaimer: This article is not an introduction to redux. There are plenty of good resources to get started with redux (see below) and I suggest you begin with them. This article isn’t about react either. The term “lifecycle” is used in context of redux data flow and operations.

In just three years, redux has become quite a popular choice among frontend developers as the state management library of choice. Even more so in the react ecosystem.

It’s amazing (and impressive, really) how small and simple the codebase is. And yet it can handle highly complex situations with easily. Perhaps this simplicity of redux is a major driving factor for its widespread popularity.

Internally, redux only works with synchronous data flow and doesn’t come with many “moving parts” or magic methods like some other solutions.

The most basic redux setup can be thought of as a single loop of state updates that is made up of just 2 components:

  • The state tree
  • The reducer function

Actions are accessory objects that are required to figure out how to update the state for the next iteration.

The reducer function takes in the previous state and the dispatched action as its arguments and returns the next state. If there are no changes needed, it returns the previous state as-is. Otherwise, it creates new state and returns it.

It is a common practice to split our state tree into multiple slices and write a separate reducer for each state slice. Actions may or may not be concerned with multiple state slices. This process of breaking our reducers into smaller and easier to understand pieces in a process called Decomposition.

Independent Reducer Lifecycles

We can now combine several independent reducers back into a single reducer (often called the “root” reducer) and create a redux store using it.

import { combineReducers } from 'redux'
import fooReducer from './foo'
import barReducer from './bar'

const rootReducer = combineReducers({
foo: fooReducer,
bar: barReducer,
})

Alternatively, we could exclude the ‘Reducer’ suffix from our imports to make the code lighter.

import foo from './foo'
import bar from './bar'

const rootReducer = combineReducers({
foo,
bar,
})
Basic Redux Lifecycle

When an action is dispatched, the root reducer receives it and passes the same action object to all reducers for them to react to it independently. However, instead of passing the entire state tree to each reducer, redux will only pass the reducer’s exclusive state slice. The return value from each reducer is then merged back into the complete state tree.

This is the basic redux architecture and it is the most common one as well. With careful planning and smart division of the the app state most small-scale apps can work with this architecture without any added complexities.

As your state tree grows complex, you may find yourself in situations where you need to share state between different reducers. A quick fix is to combine those reducers into one. However, doing so usually creates fat reducers that don’t scale well.

If we want keep our reducers separate, we need to figure out whether we need to share the current state slice or the updated version.

Since reducers are just functions, we can pass them any number of arguments as we want. If a reducer requires state slice from another reducer, we can pass it in as the third argument.

Current-State Dependent Reducers

We cannot combine dependent reducers using the combineReducers() utility that ships with redux.

Once you go past the core use case for combineReducers, it’s time to use more “custom” reducer logic — Redux docs

Since reducers are just functions, we can pass in the state of a different reducer as a third argument.

import foo from './foo'
import bar from './bar'

function rootReducer(state, action) {
const fooState = foo(state, action)
const barState = bar(state, action, state.foo)
  return {
foo: fooState,
bar: barState,
}
}
Combining Current-State Dependent Reducers

Generally, when a reducer depends on another reducer’s state, it requires the updated state. Personally, I have never come across a situation where the old state is required. If you know of any such experiences, do share them in the comments.

Updated-State Dependent Reducers

The code looks almost the same as it was in the previous section. We have made only a small change. Instead of passing the old state in state.foo, we are now passing the updated state in fooState.

import foo from './foo'
import bar from './bar'

function rootReducer(state, action) {
const fooState = foo(state, action)
const barState = bar(state, action, fooState)
return {
foo: fooState,
bar: barState,
}
}
Combining Updated-State Dependent Reducers

Dependence on updated state of other reducers means that there is a vertical hierarchy of reducer invocation. This doesn’t mean much for trivial cases but the idea does hint the possibility of chaining reducers together. However, reducer chaining will increase the code complexity exponentially.

Note: Another alternative to the “shared-slice updates” issue would be to simply put more data into the action. You can read more about it here.

Now that we’ve seen some patterns of laying out reducers and combining them in isolation, let’s see what they look like in union. Here’s a list of key things to observe:

  • Each reducer receives its exclusive slice of state as the first argument
  • Each reducer receives the same action object as the second argument
  • Dependent reducers receive the complete state or other reducers’ state slice as subsequent arguments
  • All updated state slices are merged together into a single state tree
  • Dependent reducers can form an invocation chain
Complete Redux Lifecycle

In all of the drawings, I intentionally left out action creator(s) and the store to keep them clean and free from bloat. As you know:

  • An action creator is a plain function that create an action object.
  • The dispatch function accepts the action object and passes it to it’s store’s root reducer.
  • The store is the place where the combined state tree and the dispatch function live.
  • Each redux store has its own associated root reducer that consumes the action object and updates the store’s internal state.


Tag cloud