46

I'm creating a custom hook and would like to define an optional param so that I can pass in extra dependencies when needed. My code looks like the following snippet:

import { useEffect } from 'react';

function useCustomHook(param1, extraDeps) {
  useEffect(() => {
    // do something with param1 here
  }, [param1, ...extraDeps])
}

The react-hooks/exhaustive-deps throws a warning saying

React Hook useEffect has a spread element in its dependency array. This means we can't statically verify whether you've passed the correct dependencies

Anyone have an idea about how to address that warning? Or it's not a good practice to pass deps array to custom hook?

For those who are interested in why extraDeps is needed. here's an example:

const NewComponent = (props) => {
  [field1, setField1] = useState()
  [field2, setField2] = useState()

  // I only want this to be called when field1 change
  useCustomHook('.css-selector', [field1]);

  return <div>{field1}{field2}</div>;
}
8
  • This seems like a bad idea, but it would be helpful for you to show in your snippet how you are actually using extraDeps. If the effect doesn't leverage extraDeps in some way, then it doesn't make sense for it to be part of the dependency array. Commented May 22, 2019 at 18:25
  • @RyanCogswell I'm not directly using extraDeps in my customHook. I added extraDeps as an optional param so that when calling useCustomHook we can pass extra info into dependencies array to avoid unnecessary calls triggered by re-renders. Commented May 22, 2019 at 18:47
  • I don't understand what you're trying to accomplish. Passing more dependencies can only increase the number of unnecessary executions of the effect, it won't help avoid them. If you aren't using it in the hook, it shouldn't be in the dependencies array. Commented May 22, 2019 at 18:51
  • @RyanCogswell when using this hook in a component which holds a state with many fields (e.g. state = { field1, field2, field3 }), If we want to trigger the hook only when state.field1 changes, we need to pass state.field1 as extraDeps to the custom hooks. So when I define the custom hooks, I can't determine what dependencies would be when the hook being used in a component. Commented May 22, 2019 at 19:19
  • That would cause it to execute when either param1 changes or state.field1 changes rather than only when param1 changes. It would execute more not less. Commented May 22, 2019 at 19:21

6 Answers 6

18

I've found a useful alternative to the solutions proposed here. As mentioned in this Reddit topic, the React team apparently recommends something like:

// Pass the callback as a dependency
const useCustomHook = callback => {
  useEffect(() => { /* do something */ }, [callback])
};

// Then the user wraps the callback in `useMemo` to avoid running the effect too often
// Whenever the deps change, useMemo will ensure that the callback changes, which will cause effect to re-run
useCustomHook(
  useMemo(() => { /* do something */ }, [a, b, c])
);

I've used this technique and it worked very nicely.

Sign up to request clarification or add additional context in comments.

3 Comments

The callback variable name is quite confusing in this example. If useCustomHook indeed wants a callback as its parameter, it would be better to use useCallback() instead of useMemo(). If the parameter is an object (such as the param1 in the original example), then useMemo() is fine.
So with this example, you just used useMemo hook to memo the deps nothing else?
@AliHussnain She used useMemo to memorize the function that says "/* do something */". That function is then passed into useCustomHook and then it is used INSIDE in a useEffect(..., [callback]) as a dependency. Therefore, if the memorized function changes (because the dependencies a,b,c changed), it passes NEW function to the useCustomHook, which then causes the inner useEffect to re-run. Therefore making the useCustomHook implicitly depend on [ a, b, c ].
5

I had a similar issue, I wanted an effect to be executed whenever some extra dependencies were changed.
I didn't manage to give those extra dependencies, but instead I made it the way around by giving the caller the callback I wanted to be executed and let him use it when he needs.

Example :

// This hook uses extraDeps unknown by EsLint which causes a warning
const useCustomEffect = (knowDep, extraDeps) => {

  const doSomething = useCallback((knownDep) => {/**/}, [])
  
  useEffect(() => {
    doSomething(knownDep)
  }, [doSomething, knownDep, ...extraDeps]) // Here there is the warning
}

//Instead of this, we give the caller the callback
const useCustomEffect = (knownDep) => {

  const doSomething = useCallback((knownDep) => {/**/}, [])
  
  useEffect(() => {
    doSomething(knownDep)
  }, [doSomething, knownDep]) // no more warning

  return { doSomething }
}

// Use it like this
const { doSomething } = useCustomEffect(foo)
useEffect(doSomething, [bar, baz]) // now I can use my callback for any known dependency

1 Comment

This code does not make much sense to me, I'm not sure why it has received so many upvotes. First of all, the call of useEffect inside useCustomEffect is unnecessary and will cause the effect to be run twice when the component mounts. Second of all, knownDep is missing in the dependencies of useCallback. Third of all, you won't have any dependency check for your useEffect call.
4

The way you have defined your custom hook makes sense to me. In this case eslint cannot check your dependencies, but that does not mean that they are wrong. Simply disable the rule for this line to get rid of the warning:

function useCustomHook(param1, extraDeps) {
  useEffect(() => {
    // do something with param1 here
  }, [param1, ...extraDeps]) // eslint-disable-line react-hooks/exhaustive-deps
}

Depending on the type of param1, it makes sense to enable the dependency check for your custom hook by defining it in your .eslintrc.cjs:

'react-hooks/exhaustive-deps': ['warn', {
    additionalHooks: '(useCustomHook|useAnotherHook|...)'
}]

8 Comments

@GreenAsJade: I don't really understand your concern. That is the way that hook dependencies always work, isn't it?
I have never seen hook dependencies being used like that, and I'm not sure if that usage is supported by React. But either way, this problem has nothing to do with my suggested implementation of useCustomHook, but it is a general problem with React hooks. By enabling the react-hooks/exhaustive-deps eslint rule for useCustomHook, you will get warned when attempting to provide an unsupported dependency array.
I hope it's not becoming "argumentative", but I was attempting to illustrate that defining a custom hook in this way invites the client of the hook to make this mistake. The lint disable in the answer is hiding this from the author of useCustomHook: that lint message is trying to tell you that you are setting up a trap for the user of your hook.
I agree that disabling the lint puts the responsibility on the author of declaring the dependencies properly inside the code of useCustomHook, so additional care needs to be taken here. I disagree that it puts additional responsibility on the user of the hook. The hook will be used like any other hook with dependencies, and eslint will warn about a potentially wrong usage when configured correctly.
I'm keenly interested it this topic because a construct like this was rejected by a colleague in review. This is because it is the user of the hook (the person declaring the dependencies at the time that they call the hook) that can have a mysterious bug by using this hook, and they will not be warned.
|
1

Here's whay you could do:

Move the state to your custom hook, run effects on it and return it.

Something like:

Component.js


function Component() {
  const [field,setField] = useCustomHook(someProps);
}

useCustomHook.js

import {useState, useEffect} from 'react';

function useCustomHook(props) {

  const [field,setField] = useState('');

  useEffect(()=>{
    // Use props received and perform effect after changes in field 1
  },[field1]);

  return([
    field,
    setField
  ]);
}

1 Comment

I can't do this because different consumer components may have different rules on when to trigger this custom hook. That's why I add the optional deps array originally.
1

If you want to provide extra-deps you can use useDeepCompareEffect instead of useEffect.

https://github.com/kentcdodds/use-deep-compare-effect

Comments

-2

I think the problem lies in how you are creating the dependency array on your custom hook. Every time you do [param1, ... extraDeps] you are creating a new Array, so React always see them as different.

Try changing your custom hook to:

function useCustomHook(deps) {
  useEffect(() => {
    // do something with param1 here
  }, deps)
}

And then use it like

const NewComponent = (props) => {
  [field1, setField1] = useState()
  [field2, setField2] = useState()

  // I only want this to be called when field1 change
  useCustomHook(['.css-selector', field1]);

  return <div>{field1}{field2}</div>;
}

Hope it helps!

3 Comments

This will trigger another warning, because deps can not be checked to be an array neither.
This answer does not make any sense. It is normal with hook dependencies to recreate the array on every render. If React handled that as a change of dependencies, hooks wouldn't work at all.
Also, using the deps array directly triggers the same error.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.