Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid issue with unguaranteed useCallback deps in useEffect deps?

Tags:

react-hooks

I have recently started to use the new React Hooks API and I find it awesome!

However, I ran into a small confusion in the dependencies area.

What is this about?

Basically, my use case is pretty simple, and can be illustrated by the following pseudo code:

import React, { useState, useCallback, useEffect } from 'react'

function Component() {
  const [state, setState] = useState()

  const doStuff = useCallback(() => {
    // Do something 
    setState(result)
  }, [setState])

  useEffect(() => {
    // Do stuff ONLY at mount time
    doStuff()
  }, [])

  return <ExpensivePureComponent doStuff={doStuff} />
}

Now, the above code works fine.

But after I installed eslint-plugin-react-hooks, there is a warning. I must declare all dependencies that is use in my effects, which is, here, doStuff.

Fine, let's fix that code:

  useEffect(() => {
    // Do stuff ONLY at mount time
    doStuff()
  }, [doStuff])

Cool, no more warning!

Wait, no warning, but... no bug either?

Let's see what the docs say about useCallback:

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)

And then, about useMemo:

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

So, basically, my doStuff callback, hence my useEffect, isn't guaranteed anymore to only run at mount time? Isn't that a problem?

I understand the principles behind the eslint plugin, but it looks to me that there is a dangerous confusion between useCalback/ useMemo dependencies arrays, and useEffect one's, or am I missing something?

It might be, because even the docs say that my final code is fine:

If for some reason you can’t move a function inside an effect, there are a few more options:

● ...

● As a last resort, you can add a function to effect dependencies but wrap its definition into the useCallback Hook. This ensures it doesn’t change on every render unless its own dependencies also change


me: blah blah hooks bla

SO: What is your question? :)

Well, what do you think? Code is safe? Docs say it is, but also say it is not, because callback is not guaranteed to not change... this is a bit confusing.

Is there a bad practice in the above pseudo code? When that pattern cannot be avoided, what to do? // eslint-disable-next-line?

like image 501
Pandaiolo Avatar asked Mar 29 '19 00:03

Pandaiolo


People also ask

How do I fix missing dependencies in useEffect?

The warning "React Hook useEffect has a missing dependency" occurs when the useEffect hook makes use of a variable or function that we haven't included in its dependencies array. To solve the error, disable the rule for a line or move the variable inside the useEffect hook.

What is a missing dependency?

Missing dependencies are those dependencies that are not available in the repository, so you cannot add them to your deployment set. You can set Deployer to ignore missing dependencies when you create the project (see Creating a Project) or when you check unresolved dependencies.

Can function be a dependency in useEffect?

The useEffect hook allows you to perform side effects in a functional component. There is a dependency array to control when the effect should run. It runs when the component is mounted and when it is re-rendered while a dependency of the useEffect has changed.


1 Answers

While the docs say that

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)

it doesn't mean that useCallback is implement using useMemo and it certainly is not. So while useMemo may choose to calculate again useCallback won't update the function unless the something in dependency array changes.

Also since the setter returned by useState doesn't change, you don't need to pass it on to the useCallback

  const doStuff = useCallback(() => {
    // Do something 
    setState(result)
  }, [])

Since doStuff won't change, useEffect won't be called again apart from initial mount.

One thing however that you should keep in mind while using useEffect and useCallback is that if you dependency array in useCallback changes the callback will be recreated and hence useEffect will re-rerun. One way to prevent such scenarios is to make use of useReducer hook instead of useState and rely on dispatch to update state since it won't ever change during the course of your App interactions in a session.

import React, { useReducer, useEffect } from 'react'

const initialState = [];
const reducer = (state, action) => {
    switch(action.type) {
        case 'UPDATE_STATE' : {
            return action.payload
        }
        default: return state;
    }
}
function Component() {
  const [state, dispatch] = useReducer(reducer, initialState);


  useEffect(() => {
    // Do stuff ONLY at mount time
    dispatch({type: 'UPDATE_RESULT', payload: ['xyz']})
  }, [])

  return <ExpensivePureComponent dispatch={dispatch} />
}
like image 104
Shubham Khatri Avatar answered Sep 22 '22 16:09

Shubham Khatri