React: should state live in parents?
Published: 2023-11-21 18:07:37

(With that cliche sound of N*tfl*x series opening) _Previously in my writing..._

_Yea yea_ I wrote about Virtual DOM, but that's not what I meant. What I will write today is related with what I wrote in Component Scoping in my previous article. If you haven't read it or in case you forgot already, you can check it here (_and boost my views too_ 😌).
{% embed https://dev.to/jessicacb12/react-how-i-understand-virtual-dom-through-usestate-3f73 %}

## Table of Contents

- [Initial experiment: compare the state change in parent and child](#initial-experiment-compare-the-state-change-in-parent-and-child)
- [Second experiment: profile the re-render](#second-experiment-profile-the-rerender)
- [So, what's the verdict and what's next?](#so-whats-the-verdict-and-whats-next)
- [Further Readings](#further-readings)

## Initial experiment: compare the state change in parent and child
So actually, I have been playing around with `useState`, doing some tiny experiments. My previous article was actually one of the insights I got from these experiments, and I decided to correlate it with my understanding about Virtual DOM. So now you know that I didn't just understand Virtual DOM last month ✌️

Last time, I placed the state in the `Child` component. This time, I tried to centralize all states in parent instead. To give you the big picture , here is how the `App` looks like.

```
import { useState } from 'react';
import Child from './Child';
import Title from './Title';
import Button from './Button';

export default function App() {
  const [shown, setShown] = useState(false);
  const [shown2, setShown2] = useState(false);

  return (
    <div>
        <Title/>
        <Child order={1} shown={shown} />
        <Child order={2} shown={shown2} />
        <Button order={1} onClick={() => setShown(prev => !prev)}/>
        <Button order={2} onClick={() => setShown2(prev => !prev)}/>
    </div>
  );
}
```

Now let's start to visit the components one by one.

```
export default function Title() {
    console.log('Rendering title');
    return <h1>TITLE HERE</h1>
}
```

_Yea_ very simple right for `Title`? Just a mere heading and a `console.log` to inform when this component is rendered.

```
import { useState } from "react";

export default function Child ({ order, shown }) {
    const [count, setCount] = useState(0);

    console.log('Render child', order)

    return (
        <div onClick={() => setCount(prev => prev + 1)}>
            CHILD {order} {String(shown)} count {count}
        </div>
    )
}
```

The `Child` component receives 2 props:

- `order` as the identifier of the component
- `shown` as the state that will be changed from its parent

Aside from the passed props, I also add a local state `count` for comparison later.

```
export default function Button ({ order, onClick }) {
    console.log('Render button', order)
    return <button onClick={onClick}>BUTTON {order}</button>
}
```

Lastly there's also this `Button` component with passed `onClick` which will trigger the change in `Child`, another `order` as identifier, and `console.log`.

Now let's see the result here.

![Aside from value change in Child 1, clicking the Button 1 triggers console log for Title, Child 1, Child 2, Button 1, and Button 2. Similar things happened when clicking Button 2.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yglpecdp1jdsgqu78b58.gif)

It's expected right that `Child 1` is re-rendered? Since it accepts props from its parent and the value changes, of course the component will be re-rendered

What about `Button 1`? Sure it's clicked to trigger change in `Child 1` and it accepts props too. But the value doesn't change right, so is it expected?

What about `Child 2` and `Button 2` also accept props but the value don't even change. Are they expected to get re-rendered too?

And the weirdest one is for `Title` which doesn't accept any props from its parent, yet it still gets re-rendered.

Now what about the state that only exists in `Child` component? Since its parent doesn't aware of their existence, the parent should not get re-rendered right?

![Aside from value change in Child 1, clicking Child 1 only triggers console for Child 1. Similar thing happened when clicking Child 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1u8ct5i8b2weh3gr5lxu.gif)

And yep only the `Child` itself that is re-rendered. In fact this is already spoiled in my previous article 😂 or to quote it again:

> By separating `Child` component from `App`, it is only normal if the state change in `Child` will only re-render `Child` right? After all only `Child` knows the existence of `count` state. And yes it is.

I have heard a saying that **props/state change is the one that causes re-render**. Not wrong, of course, but there seems to be a caveat.

Sure, props/state change triggers re-render, but it seems not to be the only reason. In fact, **the parent component re-render will also re-render its children**. 

I know some of you will be like:
🗣️ _That's why you should pay attention to the documentation_

And does it exist in React documentation? Yes, it does. I don't know if you guys can find the more specific statement about this somewhere in the docs, but here is the one I found in `useState` caveats

{% embed https://react.dev/reference/react/useState#setstate-caveats %}

I know that there are a lot of optimization done by React like memoization, but it seems that re-rendering its children after props / state change is remain the component **default behavior**.

And this actually leads us to one of the most famous performance-related questions:

> Is re-render a bad thing?

## Second experiment: profile the re-render
In general, labeling something as 'good' or 'bad' is subjective, right? Something that's considered 'good' can be considered 'bad' instead by others, depends on circumstances and preferences. Just like instant noodle that's not healthy, but healthier for your budget 🍜

_Goodness how random I am_ 🤣 but what I'm saying is, we can't judge anything without 'proof'. Only after we prove it, we can take decision whether to improve it or let it slide for higher priorities.

So let's start by separating the code in `App` into component called `Parent` as shown below.

```
import { useState } from 'react';
import Child from './Child';
import Title from './Title';
import Button from './Button';

export default function Parent() {
  const [shown, setShown] = useState(false);
  const [shown2, setShown2] = useState(false);
  const [count, setCount] = useState(0);

  return (
    <div>
        <Title/>
        <Child
            order={1}
            shown={shown}
            count={count}
            setCount={setCount}
        />
        <Child order={2} shown={shown2} />
        <Button order={1} onClick={() => setShown(prev => !prev)}/>
        <Button order={2} onClick={() => setShown2(prev => !prev)}/>
    </div>
  );
}
```

Doesn't look exactly the same like before right? I deliberately put out the `count` state into `Parent` and pass them to `Child 1`. I also adjusted the `Child` component as shown below.

```
export default function Child ({
    order,
    shown,
    count = 0,
    setCount = () => {}
}) {
    console.log('Render child', order)

    return (
        <div onClick={() => setCount(prev => prev + 1)}>
            CHILD {order} {String(shown)} count {count}
        </div>
    )
}
```

Now for the `App`, aside from importing the `Parent` I wrap it with `Profiler` component which is used to measure the rendering performance. Don't worry if you have never heard of this, you can check the documentation later attached at the end of this article. You'll see how this works after this.

```
import { Profiler } from "react";
import Parent from "./Parent";

export default function App() {
  const handleRender = (
    _id,
    phase,
    actualDuration,
    baseDuration
  ) => {
    console.log(
      'RERENDERING',
      phase,
      'ACTUAL',
      actualDuration,
      'BASE',
      baseDuration
    );
  }

  return (
    <Profiler id="Parent" onRender={handleRender}>
      <Parent/>
    </Profiler>
  );
}
```

To give you a brief explanation of parameters in `onRender`:

- `phase` indicates whether the component is just mounted or there has been state change **either** in the component or its children
- `actualDuration` is, to make it short, how long the whole wrapped component is rendered.
- `baseDuration` is the one that I bet all of you gets confused with `actualDuration` 😂 same guys same. Now this one is the **estimation** how long the component will get rendered.

Oh and also, please ignore the `console.log` that is printed twice per component re-render. The component is still only gets rendered once per re-render. This is how it looks like.

![State in parent performance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dn6rkbbetzippdxts7he.gif)

Either clicking on `Child 1` or `Button 1` trigger the re-render for the whole components. On mounted it already takes 8ms, longer than estimated 5.5ms to render the whole components. The re-render takes shorter time (1 - 3ms) than the initial render as mentioned in React `Profiler` documentation.

Now let's put back the `count` state into the `Child` again. To avoid confusion, here is how `Parent` component become.

```
import { useState } from 'react';
import Child from './Child';
import Title from './Title';
import Button from './Button';

export default function Parent() {
  const [shown, setShown] = useState(false);
  const [shown2, setShown2] = useState(false);

  return (
    <div>
        <Title/>
        <Child order={1} shown={shown} />
        <Child order={2} shown={shown2} />
        <Button order={1} onClick={() => setShown(prev => !prev)}/>
        <Button order={2} onClick={() => setShown2(prev => !prev)}/>
    </div>
  );
}
```

And the `Child` component looks like this again.

```
import { useState } from "react";

export default function Child ({ order, shown }) {
    const [count, setCount] = useState(0);

    console.log('Render child', order)

    return (
        <div onClick={() => setCount(prev => prev + 1)}>
            CHILD {order} {String(shown)} count {count}
        </div>
    )
}
```

And here is the result.

![State in child performance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5nsruuruvg2s031a14kz.gif)

It takes 7.3ms to perform the initial render for the whole components, but I don't think we can consider the 0.7ms difference is the improvement effect since it's still a small difference. This can be biased by the cache and stuffs anyway.

But what's _actually_ significant is it only takes 0.8ms to render the change on clicking `Child` component. While it still takes the usual 1 - 3ms to render the change on clicking `Button` component.

## So, what's the verdict and what's next?
Now we know that it definitely takes shorter time to re-render only the child than the whole component, which, is of course a good thing.

Aside from performance, there's actually no use right to put `count` state in `Parent` component? If only `Child` component is the one that needs it, why putting it in its parent? There is no need for its parent component to know the existence of this state unless they also consume it. So in a way, this also encourages us to maintain better architecture right?

But come to think of performance again, 3ms to 0.8ms is not that _huge_ difference right? They are still in millisecond anyway, so small that user won't directly feel the difference. But what about for a page with more complex component structures and more complex data? Would that still be the same?

Lots of more questions right instead of answer? 😂 I didn't plan to give you the exact answer when writing this. But hopefully I manage to intrigue your curiosity to learn deeper, as I plan to research more about re-render in my next writings.

Until then, see ya! 👋

## Further Readings

- [React useState](https://react.dev/reference/react/useState)
- [React Profiler](https://react.dev/reference/react/Profiler)