Decoding React: Navigating the Intricacies of State Preservation and Resetting

Decoding React: Navigating the Intricacies of State Preservation and Resetting

Let's face it! A good number of React developers do not really understand how state is tracked across different component re-renders. A common pitfall is not knowing when a component's state will be preserved and when it will be reset.

We could infer some questions regarding state behaviours in React; when is a component state preserved? when is it destroyed or reset? what semantics are involved in state preservation and resetting?

All these are valid questions which we need to clarify but before we dive in, we need to first answer the foremost question:

What Controls A Component's State in React?

State is determined by React's render tree and NOT by the component or JSX markup

The render tree is the relationship model between the different components in a React app. It describes the view of your UI in a single render.

Let us look at a brief example:

import DayView from "./DayView";
import YearView from "./YearView";
import MonthView from "./MonthView";

export default function App() {
  return (
    <>
      <DayView currentMonth format="DD" />
      <YearView>
        <MonthView month={"January"} />
      </YearView>
    </>
  );
}

The above component can be represented as a tree like the hand-drawn diagram below:

Each node in the tree is a component as structured in a single render; therefore the tree node or structure will change during conditional renders.

Now, How Does The Render Tree Tie to State?

Okay! Now we've established what the render tree is, how on earth does it tie to state handling in React? Once again, let's take a simple App component that renders the same child component twice:

import { useState } from "react";

export default function App() {
  return (
    <div style={{ display: "flex", gap: "1rem" }}>
      <Counter />
      <Counter />
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);

  return (
    <div
      style={{
        border: "#000 solid 1px",
        textAlign: "center",
        padding: "0.2rem",
        width: "20%",
      }}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>Add one</button>
    </div>
  );
}

Even though these two <Counter /> components are the same, they occupy different positions in the render tree, as illustrated in the diagram above. Consequently, React isolates each component so they are independent of one another. Clicking on either <Counter /> changes its score state without affecting the other!

Each of the score state will update independently as long as they maintain their positions in the render tree!

When Will State Be Preserved?

By now, we know that state is preserved as long as the component remains in the same position in the render tree view. However, it's important to know how "strict" React applies this. Let's add a checkbox to our code to render two of the same <Counter /> component conditionally :

import { useState } from "react";

export default function App() {
  const [darkMode, setDarkMode] = useState(false);

  return (
    <div style={{ display: "flex", gap: "1rem" }}>
      {darkMode ? <Counter darkmode /> : <Counter />}
      <label>
        <input
          type="checkbox"
          checked={darkMode}
          onChange={(e) => {
            setDarkMode(e.target.checked);
          }}
        />
        Set dark mode
      </label>
    </div>
  );
}

function Counter({ darkmode }) {
  const [score, setScore] = useState(0);

  return (
    <div
      style={{
        background: darkmode ? "#323232" : "#fff",
        border: "#000 solid 1px",
        textAlign: "center",
        padding: "0.2rem",
        width: "20%",
      }}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>Add one</button>
    </div>
  );
}

What is the result? The state is not reset nor destroyed after clicking the checkbox even when it goes from one render to another. This is because the component is the same across the two different renders, and so React sees only one component and does not bother to destroy the state (even when they both have different props).

The same component in the same position in the render tree will preserve state

When Will State Be Reset or Destroyed?

Once again, we shall have a look at our basic <App /> component code, we still need our checkbox input, to conditionally render two components but this time the components are different.

import { useState } from "react";

export default function App() {
  const [darkMode, setDarkMode] = useState(false);

  return (
    <div style={{ display: "flex", gap: "1rem" }}>
      {darkMode ? <div> Check if state remains! </div> : <Counter />}
      <label>
        <input
          type="checkbox"
          checked={darkMode}
          onChange={(e) => {
            setDarkMode(e.target.checked);
          }}
        />
        Toggle this
      </label>
    </div>
  );
}

function Counter({ darkmode }) {
  const [score, setScore] = useState(0);

  return (
    <div
      style={{
        background: darkmode ? "#323232" : "#fff",
        border: "#000 solid 1px",
        textAlign: "center",
        padding: "0.2rem",
        width: "20%",
      }}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>Add one</button>
    </div>
  );
}

Clicking on the checkbox toggles two different components, the first render contains the <Counter /> component. As soon as the checkbox is clicked, the <Counter /> component is removed from the render tree and React destroys its state by default. Conclusively, we can state that:

Different components in the same position in the render tree will destroy or reset state

Cool! So What Are The Best Practices For Managing State?

The React defaults of preserving and resetting state based on the render tree positions work fine for most cases, you just need to remember how it works! However, there are indeed valid scenarios where you may want a different behaviour such as resetting state for the same component in the same position.

Take Advantage Of The "Key" Prop!

The key prop in React shines in giving components a unique identity. With the key prop, you can help React differentiate between various components even if they are in the same position on the render tree.

Using the key prop tells React to not just manage state using the render tree alone, but a component's key containing a unique identifier value.

If we modify our earlier example of two similar components occupying the same position in the render tree, by adding a key prop to each of them, we should get a different behaviour:

import { useState } from "react";

export default function App() {
  const [darkMode, setDarkMode] = useState(false);

  return (
    <div style={{ display: "flex", gap: "1rem" }}>
      {darkMode ? (
        <Counter key="first-El" darkmode />
      ) : (
        <Counter key="Second-El" />
      )}
      <label>
        <input
          type="checkbox"
          checked={darkMode}
          onChange={(e) => {
            setDarkMode(e.target.checked);
          }}
        />
        Set dark mode
      </label>
    </div>
  );
}

function Counter({ darkmode }) {
  const [score, setScore] = useState(0);

  return (
    <div
      style={{
        background: darkmode ? "#323232" : "#fff",
        border: "#000 solid 1px",
        textAlign: "center",
        padding: "0.2rem",
        width: "20%",
      }}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>Add one</button>
    </div>
  );
}

React now sees two different nodes even though they are in the same position in the render tree!

Never Nest A Component Definition Within Another Component!

A common mistake many React developers make is to define a component definition (especially if it contains state) within the main component, this might seem harmless at first look, but whenever the component re-renders, a different reference is created for the nested function. Consequently, its state is destroyed during every render; this can greatly impact performance and cause unexpected bugs!

💡
If you must nest a component definition, consider using the useCallback() hook.

Conclusion

You can solve a significant number of performance issues when you fully understand the state behaviour of React as tied to the render tree as well as your JSX markup. This becomes increasingly important as your app scales because the chances of performance pitfalls are reduced!