Best practice for indicating prop is used in useEffect or similar

Viewed 377

Often when writing a component in React+Typescript, I want to trigger a useEffect hook on one of the props. For example, consider a button that executes a different function for each consecutive mouse click:

export function ActionButton({clickActions}: {clickActions: (() => void)[]}) {

  const [clicked, setClicked] = useState(0);

  useEffect(() => {
    if (clickActions.length > 0 && clickActions[clicked % clickActions.length]) {
      clickActions[clicked % clickActions.length]();
    }
  }, [clicked, clickActions])

  return <button onClick={() => setClicked(clicked => clicked + 1)}/>
}

In this case, clients of this component, need to be aware that they somehow need to prevent clickActions from being a different instance on every render. For example, it could simply be a constant, or be memoized by using useMemo.

Is there a best practice for making my clients aware of this? Are there ways to trigger compile time errors when this rule is violated?

NOTE: I realize this particular case can be solved without using useEffect, but it's just a simple example to illustrate the pattern. I'm not interested in solving this particular problem, but in how to solve the general problem.

2 Answers

There's nothing you can do by way of TypeScript trickery to enforce that clickActions can't change every time the parent renders.

However, you can simply remove clickActions from useEffect’s dependency list. You have to be careful, in general, when doing this, but in this case, it's safe, because the callback you’re passing to useEffect synchronously executes an action when clicked changes, which means the callback will have a reference to the most recent clickActions when it needs it.

This is analogous to the sample operator in RxJS; i.e. clickActions is sampled by clicked.

useEffect(() => {
  if (clickActions.length > 0 && clickActions[clicked % clickActions.length]) {
    clickActions[clicked]();
  }
}, [clicked])

Please also note that even when the parent uses useMemo, it's not guaranteed that it won’t recreate clickActions at a time of React’s choosing:

You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.

(see useMemo docs)

It's safe to assume useCallback comes with a similar caveat, although it's not specifically mentioned in the docs.

generally if you have a function that is being passed around its important (especially if its 'created' within a function) to use useCallback when creating it. This ensures that the "props don't change" and thus preventing a rerender.

parentFunction = useCallback(()=>{
  if (specialVar==='dance'){
    return () => {
      console.log('dance')
    }
  }
  else {
    return () => {
      console.log('do the boogy')
    }
  }
}, [specialVar])

Related