Bug: shows duplicate the last elements of an array stored with useRef

3
closed
stvkoch
stvkoch
Posted 1 month ago

Bug: shows duplicate the last elements of an array stored with useRef #22564

I'm keeping the last values passed as props without force a re-render, I'm using the useRef to store the elements without re-render the output.

The weird part is that the values showed are different from what I'm storing, duplicating the last elements.

React version: 17.0.2 image

Link to code example:

https://codesandbox.io/s/stupefied-ride-m1did?file=/src/App.js

import React from "react";

const ComR = React.memo(function Compo({ id, value }) {
  const lastElements = React.useRef([0, 0, 0, 0, 0]);
  const [_, ...m] = [...lastElements.current, value]; // remove first and insert last
  lastElements.current = m;
  console.log("rendering", id, value, memo.current);
  return (
    <div>
      {id} - {lastElements.current.join(", ")}
    </div>
  );
});

export default function App() {
  const [value, setValue] = React.useState(0);

  React.useEffect(() => {
    setInterval(() => {
      setValue(Math.ceil(Math.random() * 10000));
    }, 7000);
  }, [setValue]);

  return (
    <div className="App">
      <ComR id="1" value={value} />
    </div>
  );
}

The current behavior

The console.log is showing different what is printing into the component

image

The expected behavior

expects work as linear array operations, since was called/rendered once

irinakk
irinakk
Created 1 month ago

This is the expected behaviour, because render is triggered twice in app wrapped by <React.StrcitMode /> in dev mode(wont affect production build), but the duped console logs are somehow eliminated in React 17(will fixed by #22030 ) so you only see the log of the first rendering, the second log with the final value [0, 0, 0, 1301, 1301] is hidden.

Anyway its no good adding side effects in rendering, as in your code example, you might need to use state instead.

const ComR = React.memo(function Compo({ id, value }) {
  const [lastElements, setElements] = React.useState([0, 0, 0, 0, 0]);
  React.useEffect(() => {
    setElements((els) => {
      const [_, ...m] = els;
      return [...m, value]
    })
  }, [value])
  return (
    <div>
      {id} - {lastElements.join(',')}
    </div>
  );
});
bvaughn
bvaughn
Created 1 month ago

Anyway its no good adding side effects in rendering

This is right! You can learn more here: https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects

Calling setTimeout is a side effect, which is why you have it in an effect:

  React.useEffect(() => {
    setInterval(() => {
      setValue(Math.ceil(Math.random() * 10000));
    }, 7000);
  }, [setValue]);

But modifying the ref (reading or writing) during render is also a side effect:

const ComR = React.memo(function Compo({ id, value }) {
  const lastElements = React.useRef([0, 0, 0, 0, 0]);
  const [_, ...m] = [...lastElements.current, value];

  // This is also a side effect;
  // the only time this should be done is for the "lazy initialization" pattern.
  lastElements.current = m;

I wrote a bit more about why this is on PR #18545, although we have not yet enabled this warning.

bvaughn
bvaughn
Created 1 month ago

Going to close this issue because it seems like a misunderstanding that has hopefully been rectified by the two replies above. Let me know if anything is still unclear though.