1

I am looking at the code in formik that apparently is a way around the stale closure problem with react hooks.

function useEventCallback<T extends (...args: any[]) => any>(fn: T): T {
  const ref: any = React.useRef();

  // we copy a ref to the callback scoped to the current state/props on each render
  useIsomorphicLayoutEffect(() => {
    ref.current = fn;
  });

  return React.useCallback(
    (...args: any[]) => ref.current.apply(void 0, args),
    []
  ) as T;
}

I've seen this pattern a lot in other libs but I don't understand why this cures it.

I don't understand why creating a ref in a useEffect() cures anything.

Does it silence the linter?

syjsdev
  • 1,326
  • 1
  • 9
  • 22
dagda1
  • 26,856
  • 59
  • 237
  • 450
  • You mean this would cure `react-hooks/exhaustive-deps` rule? I think because [useRef](https://reactjs.org/docs/hooks-reference.html#useref) returns an `object that will persist for the full lifetime of the component.`. The linter knows this and won't complain about missing dependency that is a ref. But even if you add ref as a dependency it won't re create your return value for the component's life cycle since ref will never be re created. – HMR Oct 01 '19 at 20:13
  • Would it not be much easier to just do: `useCallback( () => console.log('never changes during comp lifecycle'), [] );` or are you passing a closure function to useEventCallback – HMR Oct 01 '19 at 20:23
  • 1
    I found this article helpful. If you have time, read all of it, otherwise, skip to Levels 9-12: https://medium.com/@sdolidze/the-iceberg-of-react-hooks-af0b588f43fb – Arash Motamedi Oct 01 '19 at 21:47
  • @ArashMotamedi That's a nice article but does not explain the solution that this custom hook brings. Creating a function that has state in it's closure scope that will change that very state without creating a new function every time you change state. – HMR Oct 01 '19 at 22:35

1 Answers1

1

The documentation actually states:

In either case, we don’t recommend this pattern and only show it here for completeness. Instead, it is preferable to avoid passing callbacks deep down.

Let's say we can't avoid passing callbacks then the simplest way would be to use a callback for the state setter: setSomeState(currentState=>....return something based on current state)

I'm not sure how this would behave when concurrent mode is released but here is an example of how you can use a callback to the state setter:

const ParentContainer = () => {
  //list is created and maintained in parent
  const [list, setList] = React.useState([
    { id: 1, val: true },
    { id: 2, val: true },
  ]);
  //simplest way to get current list is to pass a callback
  //  to the state setter, now we can use useCallback without
  //  dependencies and never re create toggle during this life cycle
  const toggle = React.useCallback(
    id =>
      setList(list =>
        list.map(item =>
          item.id === id
            ? { ...item, val: !item.val }
            : item
        )
      ),
    []
  );
  return Parent({ list, toggle });
};
const Parent = ({ list, toggle }) => (
  <div>
    {list.map(item => (
      <ItemContainer
        key={item.id}
        item={item}
        //every item gets the same toggle function
        //  reference to toggle never changes during Parent life cycle
        toggle={toggle}
      />
    ))}
  </div>
);
//Added memo to make ItemContainer a pure component
//  as long as item or toggle never changes the (render) function
//  will not be executed
//  normally a pure component should not have side effects so don't
//  do side effects in pure compnents (like mutating rendered var)
//  it is only to visibly display how many times this function was
//  called
const ItemContainer = React.memo(function ItemContainer({
  item,
  toggle: parentToggle,
}) {
  const rendered = React.useRef(0);
  //toggling item with id 1 will not increase render for
  //  other items (in this case item with id 2)
  //  this is because this is a pure component and this code
  //  will not be executed due to the fact that toggle or item
  //  never changed for item 2 when item 1 changed
  rendered.current++;
  const toggle = React.useCallback(
    () => parentToggle(item.id),
    [item.id, parentToggle]
  );
  return Item({ toggle, item, rendered });
});
const Item = ({ toggle, item, rendered }) => (
  <div
    onClick={() => toggle(item.id)}
    style={{ cursor: 'pointer' }}
  >
    <div>{item.val ? '[X]' : '[-]'}</div>
    <div>times rendered:{rendered.current}</div>
  </div>
);

//render app
ReactDOM.render(
  <ParentContainer />,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
HMR
  • 37,593
  • 24
  • 91
  • 160