29
votes

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>;
}
6
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. - Ryan Cogswell
@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. - wei
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. - Ryan Cogswell
@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. - wei
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. - Ryan Cogswell

6 Answers

3
votes

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

2
votes

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

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

1
votes

The way you have defined your custom hook makes sense to me. I haven't been able to find any documentation about what is the official way to do this, so for now my solution is to disable the rule:

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

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 dep
cost 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.

0
votes

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
votes

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!