Since React hooks rely on the execution order one should generally not use hooks inside of loops. I ran into a couple of situations where I have a constant input to the hook and thus there should be no problem. The only thing I'm wondering about is how to enforce the input to be constant.

Following is a simplified example:

const useHookWithConstantInput = (constantIdArray) => {
  const initialState = buildInitialState(constantIdArray);
  const [state, changeState] = useState(initialState);

  const callbacks = constantIdArray.map((id) => useCallback(() => {
    const newState = buildNewState(id, constantIdArray);
    changeState(newState);
  }));

  return { state, callbacks };
}
const idArray = ['id-1', 'id-2', 'id-3'];

const SomeComponent = () => {
  const { state, callbacks } = useHookWithConstantInput(idArray);

  return (
    <div>
      <div onClick={callbacks[0]}>
        {state[0]}
      </div>

      <div onClick={callbacks[1]}>
        {state[1]}
      </div>

      <div onClick={callbacks[2]}>
        {state[2]}
      </div>
    </div>
  )
}

Is there a pattern for how to enforce the constantIdArray not to change? My idea would be to use a creator function for the hook like this:

const createUseHookWithConstantInput = (constantIdArray) => () => {
  ...
}

const idArray = ['id-1', 'id-2', 'id-3'];

const useHookWithConstantInput = createUseHookWithConstantInput(idArray)

const SomeComponent = () => {
  const { state, callbacks } = useHookWithConstantInput();

  return (
    ...
  )
}

How do you solve situations like this?

2 Answers

1
ApplePearPerson On Best Solutions

One way to do this is to use useEffect with an empty dependency list so it will only run once. Inside this you could set your callbacks and afterwards they will never change because the useEffect will not run again. That would look like the following:

const useHookWithConstantInput = (constantIdArray) => {
  const [state, changeState] = useState({});
  const [callbacks, setCallbacks] = useState([]);

  useEffect(() => {
    changeState(buildInitialState(constantIdArray));
    const callbacksArray = constantIdArray.map((id) => {
        const newState = buildNewState(id, constantIdArray);
        changeState(newState);
    });

    setCallbacks(callbacksArray);
  }, []);

  return { state, callbacks };
}

Although this will set two states the first time it runs instead of giving them initial values, I would argue it's better than building the state and creating new callbacks everytime the hook is run.

If you don't like this route, you could alternatively just create a state like so const [constArray, setConstArray] = useState(constantIdArray); and because the parameter given to useState is only used as a default value, it'll never change even if constantIdArray changes. Then you'll just have to use constArray in the rest of the hook to make sure it'll always only be the initial value.

0
jkettmann On

Another solution to go for would be with useMemo. This is what I ended up implementing.

const createCallback = (id, changeState) => () => {
  const newState = buildNewState(id, constantIdArray);
  changeState(newState);
};

const useHookWithConstantInput = (constantIdArray) => {
  const initialState = buildInitialState(constantIdArray);
  const [state, changeState] = useState(initialState);

  const callbacks = useMemo(() =>
    constantIdArray.map((id) => createCallback(id, changeState)),
    [],
  );

  return { state, callbacks };
};