DEV Community

Hari Krishnan
Hari Krishnan

Posted on

Advanced State Management in React (Container pattern)

UNDERSTANDING STATE

The main job of React is to take your application state and turn it into DOM nodes. It is just a view layer.

The key in react state is DRY: Don't Repeat Yourself. Figure out the absolute minimal representation of the state your application needs and compute everything else you need on high-demand.

For example, take the case of a fullname, consider you have a state for the first name and last name, and whenever they both change you don't have to go and update the fullname and implicitly need a state for the same. Just try computing (deriving) the fullname from the already existing states.

Here the bare minimum state will be the first name and the last name, and the fullname will actually not be a state, instead it would be computed from the bare minimal state.

What isn't state ?

  1. If it is passed down from a parent via props? If so, it probably isn't state.
  2. If it remains unchanged over time? If so, it probably isn't state.
  3. Can you compute it based on any other state or props in your component? If so, it isn't state.

One way data flow

React is all about one-way data flow down the component hierarchy. It may not be immediately clear which component should own what state.

Props vs State

Props aren't necessarily state, but they are usually someone else's state, may be the immediate parent or the top most parent's state. They can be both state as well as just unchanged variables or properties.

State is created in any component and stays in the component. It can be passed down to a children as its props.

Kinds of State

States created in various regions of the application are not equal. They have various kinds.

1. Model data state : State that is retrieved from the side effects, from the server or external sources which serves as basic information for constructing the component.

2. View/UI state : State that is just responsible for updating the view. For example: a state which handles a modal dialog's open or closed state.

3. Session state : A very good example for the session state would be the auth state, handling whether any user is logged in or not.

4. Communication : The loader, error or the success state which indicates in what stage the component is in terms of fetching data from external sources (side effects).

5. Location : This state indicates where we are actually in the application. We actually use pre-defined hooks like useLocation for obtaining such states. But consider if we have a user-defined state for maintaining the current location.

State relative to time

It always makes sense to think about state relative to time.

1. Long-lasting state : This is likely the data in your application.

2. Ephemeral state: Stuff like the value of an input field that will be wiped away when you hit enter.

Ask these questions yourself

  1. Does an input field need the same kind of state management as your model data ?
  2. What about form validation and where to have their state ?
  3. Does it make sense to put all your data in one place or centralise it (something like Redux)?

There are advantages and disadvantages to both.

Basic react component state

Let's start with the simplest react component. The below code contains a functional component that will implement just a counter problem. It will have three buttons : increment, decrement and reset.

import { useState } from "react"

export const Counter: React.FC = () => {

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

    return <div>
        <p>Counter value is {count}</p>
        <button onClick={() => {setCount(count + 1)}}>Increment</button>
        <button onClick={() => setCount(count - 1)}>Decrement</button>
        <button onClick={() => setCount(0)}>Reset</button>
    </div>;
}
Enter fullscreen mode Exit fullscreen mode

Consider the following code below, where we will call setState thrice in sequence and console log the count in the end. Guess what would be the value of the count ?

import { useState } from "react"

export const Counter: React.FC = () => {

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

    const handleIncrementThrice = () => {
        setCount(count + 1);
        setCount(count + 1);
        setCount(count + 1);
        console.log("count value "+count);
    }

    return <div>
        <p>Counter value is {count}</p>
        <button onClick={handleIncrementThrice}>Increment thrice</button>
    </div>;
}
Enter fullscreen mode Exit fullscreen mode

We will get the value in the console as 0 when the Increment thrice button is clicked, but some would think the value printed in the console would be 3.

That's not the case because setState is asynchronous. The reason for it being async is that React is trying to avoid unnecessary re-renders.

React will receive three setState with count + 1, it will batch them up and identify that all three are the same and then effectively make the change by updating only once with the latest value set by setState i.e the thirdCallToSetState. Internally react actually does this :

Object.assign({
 {}, 
 yourFirstCallToSetState,
 yourSecondCallToSetState,
 yourThirdCallToSetState
});
Enter fullscreen mode Exit fullscreen mode

Also have a look over the below code, it call setState to increment the count by 1,2 and 3 in sequence

import { useState } from "react"

export const Counter: React.FC = () => {

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

    const handleIncrementThrice = () => {
        setCount(count + 1);
        setCount(count + 2);
        setCount(count + 3);
    }

    return <div>
        <p>Counter value is {count}</p>
        <button onClick={handleIncrementThrice}>Increment thrice</button>
    </div>;
}
Enter fullscreen mode Exit fullscreen mode

The count in the UI will get the value as 3, and not 6 from the initial state 0. So React purely batches up async setState calls when we pass in just values, and will update the UI with the latest call, in here it will execute only setCount(count + 3).

How to execute all the three setState calls then ?

The fact here is that setState accepts a function and that function will perform the state update and return the new state and will behave as expected. So when you pass functions to setState it plays through each of them.

import { useState } from "react"

export const Counter: React.FC = () => {

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

    const handleIncrementThrice = () => {
        setCount((count) => count + 1);
        setCount((count) => count + 2);
        setCount((count) => count + 3);
    }

    return <div>
        <p>Counter value is {count}</p>
        <button onClick={handleIncrementThrice}>Increment thrice</button>
    </div>;
}
Enter fullscreen mode Exit fullscreen mode

But, the more useful feature is that it gives you some programmatic control like imposing checks before updating the state. If you want to use this increment method in some other place of the application as well, you can move it to a common shared file. So you can declare state changes separately from the component classes.

function incrementByValue(count: number, incValue: number): number {
    if(count > 10) return count; 
        return count + incValue;
}
Enter fullscreen mode Exit fullscreen mode

Patterns and Anti-Patterns

1. State should be considered private data. You will need that for that component or you can pass it via props to its children. But modifying the state outside of any component is not needed basically except in unmanageable scenarios.

  1. Don't derive computed values in the render method, instead write a method or function which does the job for you in returning the derived value. In simple terms do not bloat the render method. Consider the below example :
type UserProps = {
    firstName: string;
    lastName: string;
}

export const User: React.FC<UserProps> = ({firstName, lastName}) => {
    // Do not do this

    // return <div>
        // <p>Full name is {firstName + ' ' + lastName}</p>
    // </div>;

    // Instead just derive in a variable or declare a method 
    // in cases where complex transformations are required

    const fullName = firstName + ' ' + lastName;
    return <div>
        <p>Full name is {fullName}</p>
    </div>;
}
Enter fullscreen mode Exit fullscreen mode

3. Don't use state for things you're not going to render.

Take care of not defining props inside the component's state object that it will never change.

4. Use Sensible Defaults :

For example if your api is going to return an array, then you
should have the default state to be an array. Otherwise what will happen is if that api call is going to take longer than what we thought, it would create a mess.

STATE ARCHITECTURE PATTERNS

Normally, react state is stored in a component and passed down as props to its children. We need to consider the actual state as private, we can show it to all the children, but if we need to change it everything needs to go back to the place where the state was defined.

Data down. Events up

Identify every component that renders something based on the state. Then find a common owner (a single component above all the components that need the state in the hierarchy).

Either the common owner or some other component up in the hierarchy should own the state. If you can't find a component where it makes sense to own the state, create a new component simply for holding the state and it somewhere in the hierarchy above the common owner component. Here the component's whole idea is to just hold the state.

Three different patterns

Lifting state with the Container pattern

Container pattern draws a line between state and presentation. The presentational components will receive props and render UI. It becomes very easy to test, consider we are writing unit tests, we can just pass down props to the presentation layer and check whether the component is rendering as expected.

It not only lifts the state, the container is also held responsible for data-fetching. So the basic idea is making your presentation components dumb makes it easy for sharing and reusing them and also to write unit tests for the same.

The presentation components will also receive actions and pass them back to the container. The actions can be triggered from the presentation layer which serves as callback functions, for example when some state needs to be updated.

So in simple terms, if we consider the Counter feature, we will have one stateless component called Counter and another stateful component called the CounterContainer.

Code for presentation layer :

// PRESENTATION LAYER

export type CounterProps = {
  count: number; // state from container
  onIncrement(): void; // actions from container 
  onDecrement(): void;
  onReset(): void;
};

export const Counter: React.FC<CounterProps> = ({
  count,
  onIncrement,
  onDecrement,
  onReset,
}) => {
  return (
    <>
      <p>Counter value is {count}</p>
      <button onClick={onIncrement}>Increment</button>
      <button onClick={onDecrement}>Decrement</button>
      <button onClick={onReset}>Reset</button>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Container component which holds state :

import { Counter } from "./Counter";
import { useState } from "react";

export const CounterContainer: React.FC = () => {
  const [count, setCount] = useState(0);

  // callback to update state in container
  const handleIncrement = () => {
    setCount(count + 1);
  };

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

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

  return (
    <Counter
      count={count}
      onIncrement={handleIncrement}
      onDecrement={handleDecrement}
      onReset={handleReset}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

Trade-offs of the container pattern :

Even if still the container pattern gives more flexibility on isolating the presentation layer, it still has to drill down props and you cannot prevent unnecessary re-renders.

May be we can use useCallback and useMemo as workarounds to prevent unwanted renders.

Top comments (0)