How to correctly implement a props callback function in the useEffect hook

Viewed 2536

I want to use useEffect() to detect changes to a state value and pass that value to a parent component using a callback function received as a prop. I can't figure out a way to do this without disabling the eslint missing dependency warning. I have this problem on both the child component and that child's child.

Here is the parent implementation:

const updateValues = (newValues) => {
  setValues({ ...values, ...newValues });
};

<GeneralUpdates onUpdate={updateValues} />

Here is the first child (GeneralUpdates):

const [values, setValues] = useState({
    name: '',
    description: '',
  });

// This handles form input changes
const handleChange = (prop) => (event) => {
  setValues({ ...values, [prop]: event.target.value });
};

useEffect(() => {
  onUpdate(values);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [values]);

<FilesUpload handleChange={onUpdate}/>

And this is the child's child (FilesUpload):

const [featuredPhotos, setFeaturedPhotos] = useState([]);

useEffect(() => {
  handleChange({ featuredPhotos });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [featuredPhotos]);

Adding handleChange as a dependency results in an infinite re-rendering loop. I've tried every solution I can find but must be missing something here.

3 Answers

A colleague helped me find a solution to this—passing a named function to useEffect rather than the anonymous one:

const updateCallback = () => {
    onUpdate(values);
  };

useEffect(updateCallback, [values]);

There isn't any problem with disabling eslint for this particular rule if you know 100% what you are doing.

However you can still get around this without disabling eslint by using useCallback hook for handleChange function being passed from parent if parent is a functional component

// in parent
const handleChange = useCallback(({featuredPhotos}) => {
    // Do what you want to do here
}, []);

If parent is not a functional component, make sure you aren't using arrow function in render while passing it as props and you should be ok

handleChange = ({featuredPhotos}) => {
   ...
}

render() {
   return (
       <Child handleChange={this.handleChange} />
   )
}

EDIT:

for your case, you can update the implementation by using useCallback for updateValue function and inside it use functional setState

const updateValues = useCallback((newValues) => {
  setValues(prevValues => ({ ...prevValues, ...newValues }));
}, []);

I'm adding one more solution which keeps also eslint happy:

const callback = useRef();
callback.current = onUpdate;     

useEffect(() => {
  callback.current(values);
}, [values, callback]);
Related