3

I can turn that rule of for entire array:

useEffect(() => {
    if (id) {
        stableCallback(id);
        dynamicCallback(id);
    }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, dynamicCallback]);

But my preference would be to achieve something along these lines (pseudocode):

useEffect(() => {
    if (id) {
        stableCallback(id);
        dynamicCallback(id);
    }
// eslint-disable-next-line react-hooks/exhaustive-deps stableCallback
}, [id, dynamicCallback]);

In my imagination, the usage of "stableCallback" does not trigger the warning, but if a new dependency emerges within it, I will see the warning about it (I know that if stableCallback is not changed then it should not matter - but that's only an example).

Is there a rule similar to exhaustive-deps, or any alternative approach that would allow me to utilize it in a similar manner?

I haven't found any alternative to it.

Deykun
  • 1,298
  • 9
  • 13

3 Answers3

3

You cannot do this and there is no rule for excluding a specific dependency. In fact, you don't have to follow Eslint, especially when it comes to including dependency, sometimes it suggests including values that you don't really want to include.

You should think of dependencies as data that once change value between renders, the corresponding hook is triggered.
If one dependency is immutable (string, number, boolean) then there is no problem but when it is mutable (object, array) then you have to memorize it using useMemo to avoid triggering the hook on every component render.

Now when you include a callback function as a dependency, this will make the hook run each time this function is recreated, I don't think you want to achieve that. maybe you just need to keep the callback function recreated each time it needs to, and trigger useEffect to run only when id is changed:

const dynamicCallback = useCallback((id) => {
  console.log(id + x + y);
},[x,y])

//...
useEffect(() => {
 if (id) {
   stableCallback(id);
   dynamicCallback(id);
 }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);

This way, the most recent version of dynamicCallback will run each time id changes


If you really want to trigger useEffect every time dynamicCallback is recreated, maybe you could make it a simple function inside useEffect and include the necessary dependencies right there, so whenever the effect is triggered, the function is recreated and then called

useEffect(() => {
 const dynamicCallback = () => {
   console.log(id + x + y)
 }
 dynamicCallback();
}, [id, x, y]);
Ahmed Sbai
  • 10,695
  • 9
  • 19
  • 38
  • 1
    The reason I drew attention to this question wasn't to understand more about *why* and *how* dependencies work (yes, they are just a list of observables we listen to in order to determine whether or not the hook should fire). What's important to me is being able to differentiate between "I forgot to add a dep, oops", and "This one is explicitly excluded because I only want it to fire when X updates". – brandonscript Aug 04 '23 at 22:49
  • @brandonscript You cannot differentiate. And in fact you dont have to, Eslint is just a tool, without it, you can include whatever you want. Just disable it for the dependencies line and include what you have to, thats it, sadly there is no rule similar to the one you are looking for – Ahmed Sbai Aug 04 '23 at 23:19
  • I agree with @brandonscript 's reasoning. I would prefer and feel more comfortable if I could communicate to the rest of the team that one specific prop should not retrigger the hook, but I still want them to know that the hook may have a missing dependency if they happen to add it. "Eslint is just a tool" isn't the answer to that (in my opinion, a real) problem. – Deykun Aug 05 '23 at 17:04
  • @Deykun I agree, the question is "Is there a rule similar to exhaustive-deps, or any alternative approach that would allow me to utilize it in a similar manner?" and my answer was "no" if you want them to know that the hook may have a missing dependency you just leave a comment as we usually do when working in team and by mentioning ""Eslint is just a tool" " I wanted to say that you don't have to follow it when it suggests including some dependencies because it is just a tool and if you don't use it you won't face that so how can it lead to a real problem? – Ahmed Sbai Aug 05 '23 at 17:17
1

Not a clean solution but it will let you mark the variable as intentionally ignored and perhaps could be easily implemented by 'react-hooks/exhaustive-deps' contributors.

Pass the dependency you intentionally ignore with a void operator:

useEffect(() => {
  if (id) {
    stableCallback(id);
    dynamicCallback(forgottenId);
  }
}, [
  void stableCallback, 
  id, 
  dynamicCallback
]);

This way, the dependency is listed in dependencies and won't cause useEffect to retrigger on change.

However 'react-hooks/exhaustive-deps' will complain about complex dependency anyways and won't see its literal, but it should be easily implemented in:

https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js (as else if near if (declaredDependencyNode.type === 'Literal') {).

brandonscript
  • 68,675
  • 32
  • 163
  • 220
0

I've spent quite a bit of time on this since I assigned the bounty to it, and I think that building semantically well-named custom hooks may be a great way to handle this. Instead of using a useEffect in place, you can create a custom hook that takes the specific dependency as a prop:

const useIdChanged = (onChange: (...args: unknown[]) => void, id: string) => {
  // very important that you call, not return the useEffect here
  useEffect(() => {
    onChange();
    // eslint-disable-next-line react-hooks/exhaustive-deps
    // or if you memoize the `onChange` function in the parent,
    // you can safely pass it as a dependency here, too
  }, [id]);
});

// ...

const SomeComponent = () => {

  useIdChanged(() => {
    if (id) {
      stableCallback(id);
      dynamicCallback(id);
    }
  }, id);

  return <></>

};

Despite the fact that you still have to use the eslint-disable-next-line flag, because you gave the hook a specific meaningful name, and the observable(s) are explicitly passed as named props, it's got some robust protection against "I don't know what this hook is doing, therefore, I will change it".

brandonscript
  • 68,675
  • 32
  • 163
  • 220