1

I’ve got a useCallback that updates state, but because it requires that state as a dependency, it creates an infinite loop when it updates the state. I’m using useImmer here, but it happens when using plain useState, too.

const [panels, updatePanels] = useImmer({
    activePanel: 0,
    validPanels: [],
});
const onValidatePanel = useCallback(isValid => {
    const panelIndex = panels.validPanels.indexOf(panels.activePanel);

    // Add
    if (panelIndex === -1 && isValid) {
        updatePanels(draft => {
            draft.validPanels.push(draft.activePanel);
        });
    // Remove
    } else if (panelIndex > -1 && !isValid) {
        updatePanels(draft => {
            draft.validPanels.splice(panelIndex, 1);
        });
    }
}, [panels]);

Basically when an index is added or removed, panels changes, triggers onValidatePanel again, and re-adds the index, and on and on…

How might I work around this?

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
Brandon Durham
  • 7,096
  • 13
  • 64
  • 101
  • Either separate the `panels` state into separate `activePanel` and `validPanels` state or don't tag `panels` as a dependency of your callback. I would prefer the former. – Dominik Jul 13 '21 at 22:09
  • Can you show how the `onValidatePanel` callback is hooked up? Because changing `panels` will just rebuild the callback but it won't actually trigger it... – gerrod Jul 13 '21 at 22:15
  • I'm not super familiar with immer (or the useImmer hook), but my hunch is that if you could access the `panels` state from draft in the functional update you could completely eliminate the need for `panels` as a dependency and eliminate the dependency cycle. – Drew Reese Jul 13 '21 at 22:20
  • 1
    If you want to compute the value of state using previous state (e.g. panels.validPanels), I'd look at using the functional form of `setState`. You can receive previous state as an arg rather than specify it as a dependency. (https://reactjs.org/docs/hooks-reference.html#functional-updates) – LeviJames Jul 13 '21 at 22:20
  • `useCallback` should not by itself trigger an infinite loop. That's only going to happen if you have it in the dependency array of a `useEffect`, and call it unguarded from within that effect. I presume you have one but have not included it? The functional state update is probably the right way to go anyway, but something else to consider. – lawrence-witt Jul 13 '21 at 22:46

1 Answers1

1

I don't think you need the dependency array populated at all, you can access the panels state from the draft copy in the functional state updater function.

const onValidatePanel = useCallback(isValid => {
  updatePanels(draft => {
    const panelIndex = draft.validPanels.indexOf(draft.activePanel);

    if (panelIndex === -1 && isValid) {
      // Add
      draft.validPanels.push(draft.activePanel);
    } else if (panelIndex > -1 && !isValid) {
      // Remove
      draft.validPanels.splice(panelIndex, 1);
    }
  });
}, []);
Drew Reese
  • 165,259
  • 14
  • 153
  • 181