Skip to content

Mimic class properties with useEventCallback#1566

Merged
jaredpalmer merged 3 commits intonextfrom
feat/protect-internal-formik
May 31, 2019
Merged

Mimic class properties with useEventCallback#1566
jaredpalmer merged 3 commits intonextfrom
feat/protect-internal-formik

Conversation

@jaredpalmer
Copy link
Copy Markdown
Owner

@jaredpalmer jaredpalmer commented May 30, 2019

In v1, all helper methods we class properties. This meant that they were bound to the instance of the class. However, with hooks, there is no concept of this and each helper method is wrapped in useCallback. However, certain behaviors (e.g. auto-saving) are not possible because of circular useCallback dependencies. For example, submitForm depends on values, so if you change values, then submitForm will be different. This prevents you from debouncing submitForm in response to values because you get a brand new function on each change. Sadness.

The suggested solution in the React docs is to use this hook:

function Form() {
  const [text, updateText] = useState('');
  // Will be memoized even if `text` changes:
  const handleSubmit = useEventCallback(() => {
    alert(text);
  }, [text]);

  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}

function useEventCallback(fn, dependencies) {
  const ref = useRef(() => {
    throw new Error('Cannot call an event handler while rendering.');
  });

  useEffect(() => {
    ref.current = fn;
  }, [fn, ...dependencies]);

  return useCallback(() => {
    const fn = ref.current;
    return fn();
  }, [ref]);
}

This PR adds this hook internally and utilizes it where necessary (on any callback that relies on state). It also adds a new DebouncedAutoSave example to the examples directory.

@jaredpalmer jaredpalmer requested a review from Andreyco May 30, 2019 16:59
@jaredpalmer jaredpalmer merged commit e51f09a into next May 31, 2019
@VanTanev
Copy link
Copy Markdown

VanTanev commented Jun 1, 2019

useEventCallback() can be written as

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
  React.useLayoutEffect(() => {
    ref.current = fn;
  });

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

as per facebook/react#14099 (comment)

To me this would make it more clear that useEventCallback() is essentially a hack to have a callback which has the entire current state/props, and not only the deps, but the identity of the callback itself doesn't change, making it safe to send downstream and not cause rerenders.

That is to say, even the current version does essentially this, but useLayoutEffect() makes it more explicit than useEffect() + deps + throw on incomplete render.

Edit Another thing that falls off from this change is, now whenever useEventCallback() is used, it looks weird, because it has no deps. And, I think it's good to look weird, because it /is/ weird. It makes it more clear that this isn't a normal hook.

@jaredpalmer jaredpalmer deleted the feat/protect-internal-formik branch December 12, 2019 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants