9

I have a question regarding eslint-plugin-react-hooks.

I wanted to reduce the boilerplate code of doing a API call and storing the result into state so I created a custom hook:

export const loading = Symbol('Api Loading');
export const responseError = Symbol('Api Error');

export function useApi<T>(
    apiCall: () => CancelablePromise<T>,
    deps: DependencyList
): T | (typeof loading) | (typeof responseError) {
    const [response, setResponse] = useState<T | (typeof loading) | (typeof responseError)>(loading);
    useEffect(() => {
        const cancelablePromise = apiCall();
        cancelablePromise.promise
            .then(r => setResponse(r))
            .catch(e => {
                console.error(e);
                setResponse(responseError);
            });
        return () => cancelablePromise.cancel();
    }, deps); // React Hook useEffect has a missing dependency: 'apiCall'. Either include it or remove the dependency array. If 'apiCall' changes too often, find the parent component that defines it and wrap that definition in useCallback (react-hooks/exhaustive-deps)
    return response;
}

Now the custom hook works great but the eslint-plugin-react-hooks not so much. The warning in my code is not a big problem. I know i can silence this warning by adding a comment:

// eslint-disable-next-line react-hooks/exhaustive-deps

The problem is that one of the custom hook arguments is a dependency list and eslint-plugin-react-hooks dose not detect missing dependencies on it. How do I make eslint-plugin-react-hooks correctly detect dependency list problems for my custom hook? Is it even possible to have such detection for custom hooks?

ParkourGrip
  • 333
  • 3
  • 11
  • 1
    Why can you not pass in apiCall as a dependency? (Where is it coming from, anyway?) If that function is changing often (which seems odd), can you memoize it using useCallback as suggested? – dangerismycat Feb 07 '20 at 08:53

2 Answers2

13

The react-hooks/exhaustive-deps rule allows you to check your custom hooks. From the Advanced Configuration options:

exhaustive-deps can be configured to validate dependencies of custom Hooks with the additionalHooks option. This option accepts a regex to match the names of custom Hooks that have dependencies.

{   
  "rules": {
    // ...
    "react-hooks/exhaustive-deps": ["warn", {
      "additionalHooks": "(useMyCustomHook|useMyOtherCustomHook)"
    }]   
  }
} 

In your .eslintrc file, add the following entry in the "rules" config:

'react-hooks/exhaustive-deps': ['warn', {
      'additionalHooks': '(useApi)'
    }],

Then you should be able to call your hook and see the linter warning and use the Quick Fix option.

enter image description here

Vinnie
  • 1,670
  • 14
  • 16
3

Looks like the dependency lists as arguments in custom hooks are not supported in eslint-plugin-react-hooks (as far as i know). There is a workaround with useCallback as dangerismycat suggested.

So instead of doing:

const apiResult = useApi(() => apiCall(a, b, c), [a, b, c]);

The same functionality can be achieved without the custom hook having a dependency list argument:

const callback = useCallback(() => apiCall(a, b, c), [a, b, c]);
const apiResult = useApi(callback);

While its a shame that it introduces a bit more boilerplate and the code is a bit harder to read, I don't mind it too much.

ParkourGrip
  • 333
  • 3
  • 11
  • Works great with hooks.macro: `const apiResult = useApi(useAutoCallback(() => apiCall(a, b, c))` – user3654410 Nov 02 '20 at 13:26
  • just to clarify, you would then pass `callback` into the deps for `useEffect`: `const useApi = (cb) => { /* ... */ useEffect(() => { const result = cb(); doSomething(result); }, cb); /* ... */ }` – ATOMP Nov 10 '20 at 22:22