0

I have a code similar to this (I know this code is stupid, but it's to get an example)

import { useState, useEffect } from "react";

const MyComponent = (props) => {
  const { names, indexes, onIndexesChange } = props;

  const [sortDir, setSortDir] = useState(1);

  useEffect(() => {
    const newIndexes = [0, 1, 2];
    newIndexes.sort((i1, i2) => {
      return sortDir * names[i1].localeCompare(names[i2]);
    });
    onIndexesChange(newIndexes);
  }, [sortDir, onIndexesChange]);

  return (
    <p>
      <button onClick={() => setSortDir(-sortDir)}>Click</button>
      <br />
      {indexes.map((index) => names[index])}
    </p>
  );
};

export default function App() {
  const names = ["Newton", "Einstein", "Pascal"];
  const [indexes, setIndexes] = useState([0, 1, 2]);

  // in real code, indexes is shared between multiple components, 
  // which it is declared above and not in MyComponent
  return (
    <div className="App">
      <MyComponent
        names={names}
        indexes={indexes}
        onIndexesChange={setIndexes}
      />
    </div>
  );
}

The above throws an expected warning

React Hook useEffect has a missing dependency: 'names'. Either include it or remove the dependency array.

I could add names to the array of dependencies but then I get an infinite loop using React 18.2.0 (I do not get one using React 18.0.0) since names is redefined on each render.

How can I get this working regardless of how names is declared (e.g., as a state or as a const variable)?


Sandbox code: https://codesandbox.io/s/clever-breeze-p1nmx7?file=/src/App.js:199-233

Holt
  • 36,600
  • 7
  • 92
  • 139
  • You can use a useMemo on names on C2 and then this names will not be modified. – mgm793 Aug 12 '22 at 07:29
  • I am not sure why you are getting infinite loops. Does `MyComponent` makes the parent `C2` re-render? If so, please include that as an example because right now it is not reproducible as can be seen [in this codesandbox](https://codesandbox.io/s/crazy-williams-8ww8li?file=/src/App.js) – Sinan Yaman Aug 12 '22 at 07:44
  • @SinanYaman Yes, `MyComponent` can make `C2` re-render, I will try to make a reproducible example. – Holt Aug 12 '22 at 07:52
  • In that case, you can try to assign a new variable inside `MyComponent` like `const _NAMES_ = Object.freeze(names)` then try to use that inside the dependency array and `useEffect`. Haven't tried, but may work. – Sinan Yaman Aug 12 '22 at 07:59
  • 1
    @SinanYaman I cannot reproduce with a simple example, so I will investigate why the parent component is re-rendered. I'll undelete the question if I manage to get more details without solving the issue. – Holt Aug 12 '22 at 08:10
  • @SinanYaman Actually I cannot delete that question... My code is closer to this https://codesandbox.io/s/clever-breeze-p1nmx7?file=/src/App.js, where I have a state that's "shared" between the component which is an array of numbers that the inner component can sort. In the sandbox. The sandbox does not trigger an infinite loop but my code does and I don't know why... Using [this](https://stackoverflow.com/a/51082563/2666289) I found that `indexes` trigger the render, but the indexes are identical (although different object), but that's also the case in the sandbox. – Holt Aug 12 '22 at 08:15
  • Is your react version 18? React 18 may be the reason it is not looping. – Sinan Yaman Aug 12 '22 at 08:17
  • @SinanYaman Actually, 18.2.0 creates an infinite loop on the sandbox (unlike 18.0.0) - https://codesandbox.io/s/clever-breeze-p1nmx7?file=/src/App.js – Holt Aug 12 '22 at 08:28
  • That is a weird one for sure :D I will try to work it through, and update my answer if I can solve the issue.. – Sinan Yaman Aug 12 '22 at 08:40

2 Answers2

1

As I said In the previous comment, you can use UseMemo to avoid the re-render.

const C2 = () => {
    const names = useMemo(() => ["a", "b", "c"], []);
    return <MyComponent names={names} />
}
mgm793
  • 1,968
  • 15
  • 23
  • Thanks, I'd like the component to work with names as it's declared in my question. I've updated the question to clarify this but basically `MyComponent` is part of a library I'm creating and I don't have control on how users will create `names` so (if possible), I'd like the component to work regardless of how `names` is created. – Holt Aug 12 '22 at 07:39
0

There are a few ways:

useMemo (for state variables)

const names = useMemo(() => ["a", "b", "c"], []);

Define names outside the component (for non-state variables)

const names = ["a", "b", "c"];
const C2 = () => {
    return <MyComponent names={names} />
}

useRef (for non-state variables)

const C2 = () => {
    const names = useRef(["a", "b", "c"]);
    return <MyComponent names={names.current} />
}

If your goal is to ensure that regardless of how the prop gets to your component, that it does not cause your library component to infinitely re-render, then you might want to consider React.memo (ymmv):

const MyComponent = memo((props: { names: string[] }) => {
  const { names } = props;

  const [index, setIndex] = useState(0);
  const [welcome, setWelcome] = useState("");

  useEffect(() => {
    setWelcome(`Welcome ${names[index]}`);
  }, [index]);

  return (
    <span>
      <Button onClick={() => setIndex((index + 1) % names.length)}>Next</Button>
      {welcome}
    </span>
  );
}, ({names: prevNames}, {names: newNames}) => {
  return prevNames === newNames || prevNames.join(",") === newNames.join(",");
});
smac89
  • 39,374
  • 15
  • 132
  • 179
  • Thanks, I'd like the component to work with names as it's declared in my question. I've updated the question to clarify this but basically `MyComponent` is part of a library I'm creating and I don't have control on how users will create `names` so (if possible), I'd like the component to work regardless of how `names` is created. – Holt Aug 12 '22 at 07:39
  • @Holt, that's something you may not be able to control. If I were writing this component, I would just assume that the users know how to work with react and therefore won't make such a silly mistake. In case you actually want to handle this case, I have updated my answer with a potential solution, although I'm not confident it will scale depending on how large the props get – smac89 Aug 12 '22 at 07:51