Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Re-Render-Loop

I am currently trying to learn about the inner workings of React in context of when a component is re-rendered or especially when (callback-)functions are recreated.

In doing so, I have come across a phenomenon which I just cannot get my head around. It (only) happens when having a state comprising an array. Here is a minimal code that shows the "problem":

import { useEffect, useState } from "react";

export function Child({ value, onChange }) {
  const [internalValue, setInternalValue] = useState(value);

  // ... stuff interacting with internalValue

  useEffect(() => {
    onChange(internalValue);
  }, [onChange, internalValue]);

  return <div>{value}</div>;
}

export default function App() {
  const [state, setState] = useState([9.0]);
  return <Child value={state[0]} onChange={(v) => setState([v])} />;
}

The example comprises a Parent (App) Component with a state, being an array of a single number, which is given to the Child component. The Child may do some inner workings and set the internal state with setInternalValue, which in turn will trigger the effect. This effect will raise the onChange function, updating a value of the state array of the parent. (Note that this example is minimized to show the effect. The array would have multiple values, where for each a Child component is shown) However this example results in an endless re-rendering of the Child with the following console warning being raised throughout:

Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

Debugging shows, that the re-rendering occurs due to onChange being changed. However, I do not understand this. Why is onChange being changed? Neither internalState nor state is changed anywhere.

There are two workarounds I found:

  1. Remove onChange from the dependencies of the effect in the Child. This "solves" the re-rendering and would be absolutely acceptable for my use case. However, it is bad practice as far as I know, since onChange is used inside the effect. Also, ESLint is indicating this as a warning.
  2. Using a "raw" number in the state, instead of an array. This will also get rid of the re-rendering. However this is only acceptable in this minimal example, as there is only one number used. For a dynamic count of numbers, this workaround is not viable.

useCallback is also not helping and just "bubbling up" the re-recreation of the onChange function.

So my question is: Do React state (comprising arrays) updates are being handled differently and is omitting a dependency valid here? What is the correct way to do this?

like image 620
newreactstarter Avatar asked Apr 08 '26 00:04

newreactstarter


1 Answers

Why is onChange being changed?

On every render, you create a new anonymous function (v) => setState([v]).

Since React makes a shallow comparison with the previous props before rendering, it always results in a render, since in Javascript:

const y = () => {}
const x = () => {}

x !== y // always true

// In your case
const onChangeFromPreviousRender = (v) => setState([v])
const onChangeInCurrentRender = (v) => setState([v])

onChangeFromPreviousRender !== onChangeInCurrentRender

What is the correct way to do this?

There are two ways to correct it, since setState is guaranteed to be stable, you can just pass the setter and use your logic in the component itself:

// state[0] is primitive
// setState stable
<Child value={state[0]} onChange={setState} />

  useEffect(() => {
    // set as array
    onChange([internalValue]);
  }, [onChange, internalValue]);

Or, Memoizing the function will guarantee the same identity.

const onChange = useCallback(v => setState([v]), []);

Notice that we memoize the function only because of its nontrivial use case (beware of premature optimization).

like image 180
Dennis Vash Avatar answered Apr 10 '26 17:04

Dennis Vash



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!