Notes on React Local Component State Management

For Intermediate to Advanced React Developers

These notes explore the local component state management mechanism provided by React (i.e., this.setState(), this.state, useState(), useReducer()) and what might lead a developer to augment it with a third-party state manager (Flux > Redux, Mobx etc.) or replace it completely.

1 : Brief overview of local component state

This will briefly outline how local state is used within a React component.

React component state (aka local state) is ideally encapsulated to a component and typically represents anything about the UI that changes over time. When building a basic counter UI the only part of the UI that changes is the count. Thus, if we were to build a Counter component, minimally, the component would have a count state (i.e., the current numeric number representing a count).

This is an example of a Counter component written as a React class component using a count state:


import React, { Component } from "react";

export default class Counter extends Component {
  state = {
    count: 0
  };

  handleIncrement = () => {
    this.setState({
      count: this.state.count + 1
    });
  };

  handleDecrement = () => {
    this.setState({
      count: this.state.count - 1
    });
  };

  reset = () => {
    this.setState({
      count: 0
    });
  };

  render() {
    const { count } = this.state;
    return (
      <section className="Counter">
        <h1>Count: {count}</h1>
        <button onClick={this.handleIncrement} className="full-width">
          Increment
        </button>
        <button onClick={this.handleDecrement} className="full-width">
          Decrement
        </button>
        <button onClick={this.reset} className="full-width">
          Reset
        </button>
      </section>
    );
  }
}

As of React 16.8 a functional component needing state can also be written using React Hooks and does not require the use of JavaScript class syntax.

The example below is the Counter component above re-written as a React function component using a count state via React Hooks:


import React, { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);

  const handleIncrement = () => {
    setCount(count + 1);
  };

  const handleDecrement = () => {
    setCount(count - 1);
  };

  const reset = () => {
    setCount(0);
  };

  return (
    <section className="Counter">
      <h1>Count: {count}</h1>
      <button onClick={handleIncrement} className="full-width">
        Increment
      </button>
      <button onClick={handleDecrement} className="full-width">
        Decrement
      </button>
      <button onClick={reset} className="full-width">
        Reset
      </button>
    </section>
  );
}

export default Counter;
      

Keep In Mind:

  1. The purpose of component/local state within a component is so that React can re-run what a component renders, even child components, when the state changes. For example, if the count of a counter changes from X number to Y number, then the UI has to be re-render (i.e., the counter component re-renders itself by re-running the render function obtaining a new value for count).

2 : Passing state to child components via props (i.e. lifting state)

These notes discuss taking state and moving it up in the component tree to then pass it from an ancestral component back down to child components (i.e. lifting state using the React Container/Smart/Controller Component Pattern).

Adding state to a component isn't a complicated or boilerplate filled exercise. However, React has this notation of passing state down to components using props. And this practice is a massive slippery slope when dealing with large component trees that have to scale. Encapsulating state to the inside of a component, while advertised as the ideal, is pragmatically never the reality when dealing with a complex web application. It seems to me that an app doesn't have to grow that big before one has to start lifting the state up, only to pass it back down via props. The idea that one can keep scope encapsulate is a nice idea, and when you can do it, you should, but this isn't solving boilerplate state management issues in large React applications. The state has to get shared and sharing is difficult.

Taking the Counter component introduced in section 1 of these notes, and the idea of lifting state, the counter code can be re-written so that the state is lifted up to a single ancestral component (i.e. CounterContainer). The React recommend way to write this component, taking full advantage of reusable "dumb" components (aka presentational components), would be to lift state up to a common "smart" container component (can't find one, then create a container component) where state can be stored in a ancestral/parent component and sent down to dumb/presentational components. Here is an example of such a re-write:

Sharing state with child components isn't that complicated. All one is doing is passing JavaScript references downward. And when communicating these ideas with a couple of components in the context of a simple counter UI, the concept of lifting state can come across as rather straight forward.

However, what happens when one is not dealing with a couple of components but thousands? What happens if we need state available to all components, and the topmost component in the component tree is the only option? What if our application is filled with state, and we have layers and layers of smart container components spread all over the tree requiring the passing of state from not just one or two components but through potentially 5 or 10 components (i.e., prop drilling)?

The application below demonstrates a slightly larger than Counter example of a growing a React application with lifted state. Study the application's usage of state. Specifically, where the state is located and how one has to manually keep moving state downwards to children at different points in the component tree to keep it as relative as possible to the components that use it (i.e., can't keep all local state at the top can we? Wouldn't that make it relative to all components in the tree?).

After examining the packing apps usage of state imagine now only using component state for an application with thousands of components with different types of state (i.e. global v.s. relative) living different life spans (i.e. non-lasting state, short-lasting state, long-lasting state).

Keep In Mind:

  1. Using only component state and state lifting container patterns in a React web application will result in tedious/repetitive, coding, editing and code reading (i.e., state down, events/functions calls up). But those who can accept this fact, do so arguing that they gain a simplistic and explicate state architecture at the cost of prop drilling.
  2. The "State Container Pattern" is the name of the pattern used to lift state to an ancestral component whose purpose is to hold state. Sometimes that component is already obvious and already available. Other times it's not apparent at all. Other times a component has to be injected into the component tree just to hold state. The idea is that one will lift state up to an ancestral component whose only purpose may be state management while also keeping that state pushed down as far as possible in the component tree, attempting to keep it as close to the component(s) as possible that use the state. When you start lifting state, dumb or presentation, components come into play (thus, re-use comes into play too e.g. a button component). Don't let the fact that React is often sold as an encapsulated state-full view be lost on you. It isn't in practice given even a slightly non-trivial implementation of React has to face the issue of state management in a tree of components where state is anything but encapsulated to a single view component (e.g., see packing app).
  3. Make sure you understand this statement entirely, "State can be passed down as props to other components. These components can consume the props or pass it even further down to their child components. In addition, child components can receive callback functions in the props from their parent components. These functions can be used to alter the local state of parent components. Basically props flow down the component tree, state is managed by a component alone and functions can bubble up to alter the state in a component that manages state. The updated state can be passed down as props again.".
  4. It will be easy to confuse, semantically, re-usable state with shared state. When discussing these topics, people will use these terms interchangeably. If you want to stay sane when discussing React state, make sure you call the same state shared by one or more components shared state. And if it's shared with all components then it is "shared global state". If a component makes use of the same kind of state, then this is "re-used state", but not the same state.

3 : Sharing state without prop drilling (via React context).

As a component tree grows, lifting state comes at a cost. Especially if the state has to be at the top of the component tree. The cost is often called, "prop drilling".

If you want to share state between components in the component tree, you will have to lift state up to a common ancestral components and then pass that state and functions that update state back down via props. If you need sharable global state you will have to lift it to the top of the tree. In either case you have a prop drilling problem unfolding.

It is at this point you either live with prop drilling hell (some claim its explicitness and manually nature resulting in boilerplate hell is a feature) or try and use the React context API to move data through the component tree without having to pass down props manually at every level.

As an example, I've re-written the previous CounterContainer component to use a CounterStateContext component (i.e. <CounterStateContext.Provider> and <CounterStateContext.Consumer>) to pass state from the Counter to sub components avoiding prop drilling on the RandomComponentX and RandomComponentY components.

Below I've re-written the above context API example to use React hooks for state and the useContext() hook instead of a consumer component (i.e. not this <CounterStateContext.Consumer>, but this: const CounterState = React.useContext(CounterStateContext);):

Keep In Mind:

  1. It is not uncommon for developers to solve prop drilling problems by reaching for Redux or Mobx. Some completely replace local state management with these tools, while others use a mix of both. Often what is going on is that developers think they can solve state issues by externally managing state, but find out that local component state has a very valid place in avoiding boilerplate hell as well. (i.e., creating a value in a store, for a non-lasting UI state value, can result in a lot of boilerplate code).
  2. "Context API is a great way to avoid prop-drilling but it isn’t a simple solution. It requires you to do some extra work to take advantage of it. Another caveat is that it tightly couples your component with a context, which means it will be a bit difficult to facilitate reuse." - How to avoid Prop-drilling in React

4 : Re-using React local state

Outlines how components can re-use state so one is not repeating themselves.

React local state (as well as any value really) can be re-used using the following three component patterns in React:

  • Higher Order Components (shown for historical purposes, many avoid this pattern today)
  • Rendered Props (Popular still but, many when possible many avoid this pattern today in favor of React Hooks)
  • Custom Hooks (Many devs use a custom hook pattern today)

Below the CounterContainer example is re-written for each pattern so that the state is not repeated for a second counter.

4.1 - Higher Order Component

4.2 - Render Props

4.3 - Hooks

Keep In Mind:

  1. Today, many are avoiding HOC and render props patterns in favor of using custom React hooks.
  2. Typically the use of hooks has to happen in a function component. But, it is possible to use hooks in a class component via the higher order component pattern (e.g. How to Use React Hooks in Class Components and Using Hooks in Classes).

5 : Using useReducer() for complex state

Outlines the use of a Redux-like pattern for managing state using the React userReducer() React Hook instead of this.setState() and this.state.

The useState() does a good job managing primitive values (e.g. string, integer, boolean) but when you want to encapsulate several values in state and need to use an Array or Object the useReducer() hook will likely be a better solution.

In the code example below the counter code, that uses React Hooks, has been re-written to make use useReducer() instead of useState().


import React, { useReducer } from "react";

const reducer = (state, action) => {
  switch (action.type) {
    case "COUNT":
      return { count: state.count + action.by };
    case "RESET":
      return { count: state.count - state.count };
    default:
      throw new Error();
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  const handleIncrement = () => {
    dispatch({ type: "COUNT", by: 1 });
  };

  const handleDecrement = () => {
    dispatch({ type: "COUNT", by: -1 });
  };
  const reset = () => {
    dispatch({ type: "RESET" });
  };

  return (
    <section className="Counter">
      <h1>Count: {state.count}</h1>
      <button onClick={handleIncrement} className="full-width">
        Increment
      </button>
      <button onClick={handleDecrement} className="full-width">
        Decrement
      </button>
      <button onClick={reset} className="full-width">
        Reset
      </button>
    </section>
  );
};

export default Counter;          
        

Keep In Mind:

  1. useReducer vs useState in React is a great article on the topic of userReducer v.s. useState question.

6 : Dealing with computed/derived values when using local component state

This section will discuss how to handle values that are derived from locale state but are not state themselves (e.g. first name + last name = full name, full name is derived not state itself)

Javascript offers the get syntax which is perfect to derive a value from actual component state. In the example below the full name is being derived from the first and last name local component state.


class FirstNameClass extends Component {
  state = {
    firstName: "Cody",
    lastName: "Sooner"
  };

  newNames = () => {
    this.setState({
      firstName: "Taco",
      lastName: "Johns"
    });
  };

  get fullname() {
    return `${this.state.firstName} ${this.state.lastName}`;
  }

  render() {
    return (
      <>
        {this.fullname}
        <br />
        <button onClick={this.newNames}>New Name</button>
      </>
    );
  }
}  
              

Here is a React Hooks version:


const Counter = () => {
  const [firstName, setFirstName] = useState("Pat");
  const [lastName, setlastName] = useState("Doe");

  const newNames = () => {
    setFirstName("Lee");
    setlastName("Homer");
  };

  const getComputed = {
    get fullname() {
      return `${firstName} ${lastName}`;
    }
  };

  return (
    <>
      {getComputed.fullname}
      <br />
      <button onClick={newNames}>newNames</button>
    </>
  );
};
                  

7 : Do I need Redux or Mobx, or something like it?

This section briefly outlines why someone would choose to manage state externally from the component tree.

You might wonder if what React offers alone is now enough to avoid external state managers like Redux or Mobx. So you might need Redux or Mobx if:

  • You want your state to live outside of the component tree in a store(s) and in a sense be in one location. Which, could mean you could keep the state for the entire application in a single JavaScript object (Making reproducing the entire state of the app easy. Imagine what that could mean for testing)
  • You want a single API to wrap the entire store(s) function (useReducer() currently creates a unique dispatch function each time).
  • You simply need a more robust management system (including middleware) for a large complex application that is being developed by a lot of developers with different skills.

References

These external resources have been used in the creation of these notes.