Overview

Managing React application state

Prior to React v16.8, there was only one way to manage your state and that was to create a React Class Component and change it with setState(). Global application state was handled this way too.

As your application grows in complexity, such as when your component heirarchy grows to multiple levels deep, application state management becomes increasingly cumbersome as you will need to pass props through all the component heirarchy, even if only the last component needs it.

This is what we call the Prop Drilling Problem, and we want to avoid this condition. Ideally, we only want to pass props to the components that need it. In fact we want to avoid having to pass props if we can help it.

React and Prop Drilling

I have created a simple application that illustrates the prop drilling problem, see how props gets passed to all the components in the heirarchy, even if only Counter component needs it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  };

  // omitted for brevity
 
  render () {
    return(
      <Container appState={
        count: this.state.count,
        increment: this.increment,
        decrement: this.decrement,
        incrementIfOdd: this.incrementIfOdd,
        incrementAsync: this.incrementAsync,
      } />
    );
  };
};

function Container (props) {
  console.log(`Container: ${props.appState.count}`)
  return (
    <Counter appState={props.appState} />
  );
};

function Counter (props) {
    return (
      <div>
        <p>
          Clicked: {props.appState.count} times
          {' '}
          <button onClick={props.appState.increment}>
            +
          </button>
          {' '}
          <button onClick={props.appState.decrement}>
            -
          </button>
          {' '}
          <button onClick={props.appState.incrementIfOdd}>
            Increment if odd
          </button>
          {' '}
          <button  onClick={props.appState.incrementAsync}>
            Increment async
          </button>
        </p>
      </div>
    );
}

Github link to the source

React sample app

How Redux solves this

As an application becomes more complex, keeping application state in a top-level component, and passing props around everywhere may not be sufficient anymore. Redux can help with this, and more. Here’s more material around Redux motivations and what the motivations were when creating it.

React and Redux

I don’t want to have to explain Redux as there are already lots of literature on this. What I would like to show is how Redux avoids the prop drilling problem by making the state available to any component that subscribes to it.

I have created a Redux version of the same application to see how Redux does it. To make Redux easier to manage in a React application (Redux can be used outside React too), the React bindings for Redux can be used, however for this exaple we are using plain Redux.

1
2
3
4
5
6
7
8
9
10
11
12
const store = createStore(counter);
const rootEl = document.getElementById('root');

const render = () => ReactDOM.render(
  <Counter
    value={store.getState()}
    onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
    onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
  />, rootEl);

render();
store.subscribe(render);

Github link to the source

The MobX approach

Redux (and the React Redux bindings), is quite full featured and robust, however, it can be a pain to use with all the boilerplate code you have to setup, don’t even get me started with using it with Typescript. When you are looking for an easier way, yet achieve somewhat the same results, you can try MobX.

I have again ported our simple example here to use MobX. None of the immutability rules that you have to remember when writing Redux reducers, welcome to reactive heaven. MobX allows you to create a store and mark them as observables. You can then instantiate an object, and mark it as an observer. Whenever you change your observables, any observer will re-rendered, just like magic.

It’s true, try it.

React and MobX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
export default class App extends React.Component {
  render () {
    return (
      <Provider counterStore={new CounterStore()}>
        <Container/>
      </Provider>
    );
  }
}

const CounterContext = createContext(new CounterStore())

function useStore() {
  return React.useContext(CounterContext)
}

const Container = () => {
  return (
    <Counter />
  );
}

const Counter = observer(() => {
    const counterStore = useStore();
    return (
      <div>
        <p>
          Clicked: {counterStore.counter} times
          {' '}
          <button onClick={counterStore.increment}>
            +
          </button>
          {' '}
          <button onClick={counterStore.decrement}>
            -
          </button>
          {' '}
          <button onClick={counterStore.incrementIfOdd}>
            Increment if odd
          </button>
          {' '}
          <button  onClick={counterStore.incrementAsync}>
            Increment async
          </button>
        </p>
      </div>
    );
});

Github link to the source

Maybe React Context Api is all you need

Before the new Context API was released, there wasn’t really an official React way of passing state around to different React components. We had to resort to the typical prop drilling method explained above. Or npm install Redux or MobX and their thousand dependencies. If you are using React, then you may already have everything you need right there.

React and Context API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
const AppContext = React.createContext();

export default class App extends React.Component {
  render() {
    return (
      <AppProvider>
          <Container />
      </AppProvider>
    );
  }
}

function Container() {
  const { counter } = useContext(AppContext); // consume context in functional component

  console.log(`Container: ${counter}`)
  return (
    <Counter />
  );
};

function AppProvider(props) {
  const [counter, setCount] = useState(0);

  var increment = () => {
    setCount(counter + 1);
  };

  var decrement = () => {
    setCount(counter - 1);
  }

  var incrementIfOdd = () => {
    if (counter % 2 !== 0) {
      increment();
    }
  }

  var incrementAsync = () => {
    setTimeout(increment, 1000);
  }

  const value = { counter, increment, decrement, incrementIfOdd, incrementAsync };
  
  return (
  <AppContext.Provider value={value}>
    { props.children }
  </AppContext.Provider>
  );
}

class Counter extends React.Component {

  static contextType = AppContext; // consume context in class based component
  render() {
    var value = this.context;
    return (
      <div>
        <p>
          Clicked: {value.counter} times
          {' '}
          <button onClick={value.increment}>
            +
          </button>
          {' '}
          <button onClick={value.decrement}>
            -
          </button>
          {' '}
          <button onClick={value.incrementIfOdd}>
            Increment if odd
          </button>
          {' '}
          <button  onClick={value.incrementAsync}>
            Increment async
          </button>
        </p>
      </div>
    );
  }

Github link to the source

Conclusion

With all these options available to us, of course it makes it confusing to decide on which method to use. You will just have to try it out for yourself. Maybe start with the simple contrived examples that I have created here. Like anything, you need hands-on experience to be able to pick one method from the other.

I used to pick Redux all the time. However, but once I saw how simple MobX makes things in comparison, I get that, and started using it more.

But ever since the new Context API has been officially bestowed the stamp of approval from React, I will probably consider it before reaching in my tool belt for Redux and MobX.

My Picks

These picks are things that have had a positive impact to me in recent weeks:

Resources

2020

DynamoDB and Single-Table Design

9 minute read

Follow along as I implement DynamoDB Single-Table Design - find out the tools and methods I use to make the process easier, and finally the light-bulb moment...

Back to top ↑

2019

Website Performance Series - Part 3

5 minute read

Speeding up your site is easy if you know what to focus on. Follow along as I explore the performance optimization maze, and find 3 awesome tips inside (plus...

Back to top ↑