React: useEffect call recursively when add functional props as dependency

Created on 1 Apr 2019  路  8Comments  路  Source: facebook/react

What is the current behavior?

  1. Define a component with useEffect and useState
const [style, setStyle] = useState({ color: "#000000" });
const [signature, setSignature] = useState("Your Name");

useEffect(() => {
    html2canvas(document.getElementById("signature-container")).then(
     canvasEl => {
       onChange(canvasEl.toDataURL());
      },
   );
}, [signature, style]);
  1. Pass onChange function as props
    <Text onChange={handleChange} />

It's warn need to add onChange prop into dependency array of useEffect but If I add onChange prop into the array, useEffect call recursively

What is the expected behavior?
Don't need to warn if user don't pass function props as dependency

Most helpful comment

OK this is interesting. Normally you'd fix this by passing an onChange with a stable identity:

const handleChange = useCallback(() => {
  /* do stuff */
}, [/* its deps */])

<Text onChange={handleChange} />

But in your case it wouldn't work because it's inside Formik's render prop callback.

I'm not sure what's the idiomatic solution here from Formik's point of view.

But in your case, it seems like it's not actually important to re-run onChange if the onChange function itself changes. Therefore, you can use a ref to explicitly track its latest value:

  const latestOnChange = useRef(onChange);
  useEffect(() => {
    latestOnChange.current = onChange;
  }, [onChange]);

And then you can all latestOnChange.current in the effect.

https://codesandbox.io/s/l5z8z3jj3z

All 8 comments

Please read this, it answers this exact question. Let me know if you still have questions afterwards!

https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies

@gaearon, thanks for the reference, I just updated the effect like this

useEffect(() => {
    function emitChange() {
      html2canvas(document.getElementById("signature-container")).then(
        canvasEl => {
          onChange(canvasEl.toDataURL());
        },
      );
    }
    emitChange();
}, [signature, style, onChange]);

but still It's calling recursively, and here is the resource usage

Screen Shot 2019-04-01 at 7 21 52 PM

useEffect(() => {
    function emitChange() {
      html2canvas(document.getElementById("signature-container")).then(
        canvasEl => {
          onChange(canvasEl.toDataURL());
        },
      );
    }
    emitChange();
}, [signature, style]);

This is working as expected but linter says It's error

may I know, what I'm missing in this point ?

@gaearon may I know, How useEffect depend on functional props ?

I think you missed the last part about useCallback in that link.

Specifically, I think you want something like this for your onChange. It鈥檚 hard to say more without seeing the whole code example as a sandbox.

A8F601B0-7E89-4E72-984D-B9DECAC32536

@gaearon thanks for your explanations and sorry for your time

This is my working implementation https://codesandbox.io/s/v0lwkokq00
This is broken implementation https://codesandbox.io/s/vw62wz5o3 I just added onChange props into the dependency list to break my implementation 馃槩

Implement notes:
Once the signature and style states change I need to parse signature preview into canvas to get signature input as image

I just add google font's stylesheet in this component because avoid load external resource until user need them, here I added in index.html because codesandbox I load bootstrap from CDN.

calculate image from signature won't work inside input change because component won't render with current value so I pick useEffect for this case

is this wrong implementation ? 馃槦

OK this is interesting. Normally you'd fix this by passing an onChange with a stable identity:

const handleChange = useCallback(() => {
  /* do stuff */
}, [/* its deps */])

<Text onChange={handleChange} />

But in your case it wouldn't work because it's inside Formik's render prop callback.

I'm not sure what's the idiomatic solution here from Formik's point of view.

But in your case, it seems like it's not actually important to re-run onChange if the onChange function itself changes. Therefore, you can use a ref to explicitly track its latest value:

  const latestOnChange = useRef(onChange);
  useEffect(() => {
    latestOnChange.current = onChange;
  }, [onChange]);

And then you can all latestOnChange.current in the effect.

https://codesandbox.io/s/l5z8z3jj3z

thanks @gaearon to spend your valuable time for this clarification, It's really help understood where I missed and how handle this kind of situation in future, thanks again!

Was this page helpful?
0 / 5 - 0 ratings