Skip to content

feat(react-x): add additionalMutableHooks setting, closes #1924 - #1931

Open
christopher-buss wants to merge 3 commits into
Rel1cx:mainfrom
christopher-buss:feat/additional-mutable-hooks
Open

feat(react-x): add additionalMutableHooks setting, closes #1924#1931
christopher-buss wants to merge 3 commits into
Rel1cx:mainfrom
christopher-buss:feat/additional-mutable-hooks

Conversation

@christopher-buss

@christopher-buss christopher-buss commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

  • Feature

Adds an additionalMutableHooks shared setting, as proposed in #1924: a regex matching custom hooks that return external mutable stores. Writes through values initialized by such a hook are exempted by react-x/immutability, the same way values returned by the built-in navigation hooks already are.

{
  "settings": {
    "react-x": {
      "additionalMutableHooks": "/^(useWorld|useMyStore)$/u"
    }
  }
}
// no longer reported with the setting above
function Component() {
  const world = useWorld();
  const onClose = useCallback(() => {
    world.set({ open: false });
  }, [world]);
  return <AnotherComponent onClose={onClose} />;
}

Registering such a hook under additionalRefHooks works today, but it declares the value a ref, so react-naming-convention/ref-name then asks for it to be renamed — the misdeclaration leaks into other rules.

Implementation notes

  • packages/shared/src/settings.ts: additionalMutableHooks follows the existing additional*Hooks shape (schema, normalized interface, toRegExp).
  • immutability/lib.ts: isKnownNonMutatingMethodCall becomes isInitializedFromMutableHook. Both the built-in NAVIGATION_HOOKS and the configured pattern are matched on the callee name from Extract.getCalleeName, which is how isUseRefLikeCall and the other isUse*LikeCall predicates match. isAPI is name-shaped too, so this is not a behaviour change; it removes the second matching mechanism from one function.
  • immutability/effects.ts: the exemption applies to every write through the value (store.count = 1, delete store.stale), not only to mutating method calls. This widens the existing navigation-hook exemption. The reason for the exemption is a property of the value — it is not a captured local — so it does not depend on the write syntax, and the useRef exemption is already syntax-uniform (ref.current = 1 and box.current.push(1) are both exempt). Reassignment of the binding itself (world = null) stays a mutation.
  • immutability.spec.diff.md: the mutation table and the exception paragraph still described the exemption as method-call-only and listed three navigation hooks; useHistory landed in 5.16.1. Updated, with the destructuring boundary recorded. The Last verified stamp is left for you.

Provenance is per declaration, not per destructured element: const { store, actions } = useMyStore() exempts writes through actions as well. That looks right for a store hook, and there is now a valid test pinning it.

Open question

additionalRefHooks shipped with a core.isUseRefLikeCall predicate plus a RuleToolkit.is entry. I did not add a core predicate here, because it would move third-party hook names (useRouter, useNavigate, …) into packages/core, which today holds React built-ins only. Happy to add core.isUseMutableStoreLikeCall + the kit entry if you would rather keep the symmetry.

Does this PR introduce a breaking change?

  • No

Checklist

  • When resolving issues, they are referenced in the PR's title
  • I have added a convincing reason for adding this feature, if necessary

Other information

Docs updated: rule immutability.mdx, configure-analyzer.mdx/.tsx, kit.mdx settings table, faq.mdx, rule CHANGELOG.md, plus regenerated typedoc output for @eslint-react/shared. Full test suite passes, including 4 new valid and 2 new invalid cases for the setting.

@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

@christopher-buss is attempting to deploy a commit to the Rel1cx's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Rel1cx
Rel1cx force-pushed the main branch 4 times, most recently from 3f9ccf0 to 716d39b Compare August 7, 2026 19:54
christopher-buss and others added 3 commits August 8, 2026 11:03
Registers custom hooks that return external mutable stores. Writes through
values from such hooks are exempt, like navigation hook values.

Also widens the navigation-hook exemption from mutating method calls to all
writes through the value; binding reassignment is still reported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drops the per-call `core.isAPICall` closures for `NAVIGATION_HOOKS` and
reuses the callee name already computed for the regex test.

Also runs the cheaper ref check before the store check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…case

The report still described the exemption as method-call-only and listed
three navigation hooks; `useHistory` landed in 5.16.1.

Adds a valid test for sibling bindings of a destructured store hook call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@christopher-buss
christopher-buss force-pushed the feat/additional-mutable-hooks branch from cba6d24 to 210037d Compare August 8, 2026 10:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant