8000 Feature Request: [eslint-plugin-react-hooks] Provide a way for custom hooks with a dependency array to accept seemingly redundant dependencies · Issue #33399 · facebook/react · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content
Feature Request: [eslint-plugin-react-hooks] Provide a way for custom hooks with a dependency array to accept seemingly redundant dependencies #33399
Open
@aweebit

Description

@aweebit

Since React doesn't provide a native way to define a dependency array for resetting state, I have to resort to a user-land implementation of this functionality. Here is the signature of the hook I use:

function useDerivedState<S>(
  initialState: S | ((previousState?: S) => S),
  deps: DependencyList,
): [S, Dispatch<SetStateAction<S>>]

Please check #33041 (comment) for details and full code.

In order to get warnings about missing dependencies, I add this rule to my ESLint config:

'react-hooks/exhaustive-deps': [
  'warn',
  { additionalHooks: 'useDerivedState' },
]

Unfortunately, that leads to warnings in cases where there shouldn't be any. Here is an example:

const [collapsed, setCollapsed] = useDerivedState(() => true, [collapsible]);

The reason is that collapsible is listed as a dependency, but not present in the initializer function's body, so the linter plugin thinks it's unnecessary. However, this is intended: it is desired that collapsed is reset to true whenever collapsible changes.

Such seemingly redundant dependencies are currently only accepted for the effects family of hooks. They got this special treatment shortly after the exhaustive-deps rule was introduced, see #14920 (comment).

The test for whether a hook belongs to the effects family is very simple: it's enough that the hook's name includes the substring "Effect" at the end of it, or followed by a symbol other than a lowercase letter.

const isEffect = /Effect($|[^a-z])/g.test(reactiveHookName);

Now, I could of course rename my useDerivedState hook to something like useDerivedStateEffect, but this would be a despicable misnomer since not only is the hook's functionality nothing like that of useEffect, but also its whole idea is to fight how effects are used all over the place for deriving state despite it being a terrible anti-pattern (#33041 has a detailed explanation).

A couple more workarounds I can think of:

  1. // eslint-disable-next-line react-hooks/exhaustive-deps
    const [collapsed, setCollapsed] = useDerivedState(true, [collapsible]);

    Simply suppressing the warning. Dirty.

  2. const [collapsed, setCollapsed] = useDerivedState(() => {
      collapsible; // eslint-disable-line @typescript-eslint/no-unused-expressions
      return true;
    }, [collapsible]);

    Forcing collapsible to be referenced in the initializer function's body, but now a different warning has to be suppressed.

    This is more verbose and a little confusing, but especially in more complex cases where we don't only have one single dependency, I would go for this option because I wouldn't want to disable the exhaustive-deps rule completely.

    (By the way, Biome's useExhaustiveDependencies rule allows ignoring only a specific dependency. This alone make me kind of want to give Biome a try. Very inspiring!)

  3. const [collapsed, setCollapsed] = useDerivedState(() => {
      return collapsible || true;
    }, [collapsible]);

    There shouldn't be any problem with this, right? Well, turns out there is, and it's that with this code, TypeScript for whatever reason decides that the type of collapsed is true rather than boolean, so it has to be specified manually:

    const [collapsed, setCollapsed] = useDerivedState<boolean>(() => {
      return collapsible || true;
    }, [collapsible]);

    I'm really clueless about why TypeScript thinks collapsible || true is of type true while a simple true expression is of type boolean. Probably I should open an issue about this in TypeScript's repo, but if anyone here has any idea, please let me know 🙏

    Anyway, I shouldn't be writing stupid code like this just to get around the linter plugin's limitations.

I suggest introducing support for the following kind of configuration:

'react-hooks/exhaustive-deps': ['warn', {
  additionalHooks: [
    { name: 'useDerivedState', allowExtraDeps: true },
    { name: 'useDefinitelyNotAnEffect', allowExtraDeps: false },
    /^(useMyCustomHook|useMyOtherCustomHook)$/,
  ]
}]

Probably quite self-explanatory, but here are some details:

  • additionalHooks can now accept objects of this shape:

    { name: string | RegExp, allowExtraDeps?: boolean }

    If allowExtraDeps is undefined, the decision on whether to report seemingly redundant dependencies should be made just the same way it is made now. Otherwise, it should be based on that property's value.

  • I think it's better not to convert the name properties from string to regexes, but treat strings as values that should be matched exactly. Before, regexes couldn't be used directly because the config file had to be in JSON format, and that is why strings were converted to regexes. With the new flat config format, that is no longer necessary to have regex support.

  • In addition to strings, regexes and object of the proposed shape, additionalHooks should also accept arrays of those values so that each one of them can be configured individually.

The proposed object shape could also be extended to allow even more flexibility, like for example what the aforementioned Biome rule offers with its closureIndex and dependenciesIndex options (this would solve #25443), or with its stableResult option (this would solve #16873)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0