npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@moznion/eslint-plugin-selective-hooks

v7.1.2

Published

Extends react-hooks/exhaustive-deps to allow fine-grained, per-dependency exceptions instead of disabling the whole rule.

Downloads

2,287

Readme

eslint-plugin-selective-hooks

eslint-plugin-selective-hooks extends react-hooks/exhaustive-deps by allowing fine-grained exceptions for specific missing or unnecessary dependencies, instead of disabling the whole rule.

Instead of disabling the entire rule and silently hiding every dependency issue:

// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
  retry(pendingIds);
}, []);

you can preserve most of the dependency checking and only excuse the dependencies you intend to:

// exhaustive-deps-except-next-line pendingIds
useEffect(() => {
  retry(pendingIds);
}, []);

In this example only pendingIds is ignored. Other missing dependencies (here retry) are still reported. The same directive also excepts unnecessary dependencies reported by the upstream rule (typically for useCallback / useMemo):

// exhaustive-deps-except-next-line keepAlive
const cb = useCallback(() => {
  return doWork(a);
}, [a, keepAlive]);

Here keepAlive is in the array but unused inside the callback — the upstream rule would flag it as unnecessary, and the directive excuses it.

Why

The motivating use cases are effects where a stale closure is intentional or a value is deliberately omitted:

  • subscription / WebSocket / SSE listeners
  • timers
  • imperative API integration
  • stable ref escape hatches
  • external store integration

Disabling the whole rule throws away the rest of the dependency graph information. This plugin keeps it: disable less, preserve more.

Installation

npm install --save-dev @moznion/eslint-plugin-selective-hooks

This plugin wraps eslint-plugin-react-hooks and declares it (along with eslint) as a peer dependency — it does not bundle its own copy. Most projects already have it; if not, install a matching version:

npm install --save-dev eslint-plugin-react-hooks

Usage (flat config)

The recommended way is to register the wrapped eslint-plugin-react-hooks under its usual name. The wrap() helper returns a shallow copy of the plugin whose exhaustive-deps rule is the selective-aware version; nothing else changes.

import reactHooks from "eslint-plugin-react-hooks";
import selectiveHooks from "@moznion/eslint-plugin-selective-hooks";

export default [
  {
    plugins: {
      "react-hooks": selectiveHooks.wrap(reactHooks),
    },
    rules: {
      "react-hooks/exhaustive-deps": "warn",
    },
  },
];

Why this shape:

  • The rule id stays react-hooks/exhaustive-deps. There is no separate rule, no "off" + alias pair to keep in sync, and no double-reporting.
  • Existing // eslint-disable-next-line react-hooks/exhaustive-deps comments and any rules: { "react-hooks/exhaustive-deps": [...] } options (additionalHooks, severity, etc.) keep working unchanged.
  • wrap() does not mutate the input plugin; the original reactHooks object is untouched.

Alternative: namespaced rule

If you would rather not wrap the react-hooks plugin, you can also enable the rule under this plugin's own namespace. In that case you must turn the upstream rule off so the two do not double-report:

import selectiveHooks from "@moznion/eslint-plugin-selective-hooks";

export default [
  {
    plugins: {
      "@moznion/selective-hooks": selectiveHooks,
    },
    rules: {
      // Turn off the upstream rule so the two do not double-report.
      "react-hooks/exhaustive-deps": "off",
      "@moznion/selective-hooks/exhaustive-deps": "warn",
    },
  },
];

A ready-made preset is exported for this style:

import selectiveHooks from "@moznion/eslint-plugin-selective-hooks";

export default [
  selectiveHooks.configs.recommended, // enables @moznion/selective-hooks/exhaustive-deps as "warn"
  {
    rules: {
      "react-hooks/exhaustive-deps": "off",
    },
  },
];

The directive

// exhaustive-deps-except-next-line <dep> [<dep> ...]

Place it on the line directly above the hook statement. The listed dependencies are excluded from the upstream dependency report (whether it's a missing or unnecessary dependency); everything else is still reported.

| Code | Result | | --- | --- | | useEffect(() => { retry(pendingIds); }, []) (no directive) | reports pendingIds and retry as missing | | // exhaustive-deps-except-next-line pendingIds | reports retry only | | // exhaustive-deps-except-next-line pendingIds retry | nothing reported | | // exhaustive-deps-except-next-line foo (not actually missing) | reports pendingIds and retry (directive is a no-op) | | useCallback(() => doWork(a), [a, keepAlive]) (no directive) | reports keepAlive as unnecessary | | // exhaustive-deps-except-next-line keepAlive | nothing reported |

Notes:

  • The directive applies to both missing and unnecessary dependency reports. (Duplicate-dependency reports from upstream are left untouched — they are almost always real bugs, not intentional.)
  • The directive must be on the immediately preceding line. A blank line between the directive and the hook invalidates it.
  • When a partial exception is applied, the upstream autofix / suggestion is dropped, because re-adding the excepted dependency would defeat the point. When the directive matches none of the actual reported dependencies, the upstream behavior (including the suggestion) is preserved unchanged.
  • Member expressions are matched by the name as the rule prints it, e.g. props.foo or (with optional chaining) props?.foo.

Fully disabling the rule

For a complete disable, use the standard ESLint directive. Note that — exactly like the upstream react-hooks/exhaustive-deps rule — the report is attached to the dependency array node, so the disable comment must sit directly above the }, []) line, not above useEffect:

useEffect(() => {
  retry(pendingIds);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

(If you went with the namespaced alternative above, the rule id in the disable comment is @moznion/selective-hooks/exhaustive-deps instead.)

(The exhaustive-deps-except-next-line directive, by contrast, is anchored to the hook statement, so it goes above useEffect as shown earlier.)

How it works

The rule wraps the original react-hooks/exhaustive-deps:

  1. It delegates to the upstream rule, intercepting its context.report calls.
  2. For each missing- or unnecessary-dependency report, it parses the kind and the dependency names out of the report message.
  3. It reads the exhaustive-deps-except-next-line directive (if any) above the enclosing statement.
  4. Excepted dependencies are removed. If none remain, the report is suppressed; otherwise the message is rewritten to list only the remaining dependencies.

Because step 2 parses the upstream message text, this plugin is pinned to a specific upstream version. This is a known piece of technical debt; future work may fork the upstream rule or propose an internal API upstream.

Scope

Implemented:

  • wrap(reactHooksPlugin) for registering the selective-aware rule under the upstream react-hooks/exhaustive-deps name
  • @moznion/selective-hooks/exhaustive-deps as a standalone alternative
  • the exhaustive-deps-except-next-line directive
  • partial suppression of missing and unnecessary dependencies with message rewrite
  • dropping fix/suggestion when a selective exception is applied
  • flat config support

Not (yet) implemented:

  • ignoring duplicate dependencies
  • a reason syntax (-- explanation)
  • unused-exception detection
  • suggestion rewriting (rather than removal)
  • TypeScript type-aware analysis

Development

This repository uses pnpm (pinned via the packageManager field). Enable it once with Corepack, which ships with Node.js:

corepack enable pnpm
pnpm install
pnpm test            # vitest + ESLint RuleTester
pnpm build           # tsc -> dist/
pnpm typecheck       # tsc --noEmit

# Static analysis (oxc)
pnpm lint            # oxlint
pnpm lint:fix        # oxlint --fix
pnpm format          # oxfmt (writes in place)
pnpm format:check

pnpm check           # typecheck + lint + format:check + test

Linting uses oxlint (config: .oxlintrc.json) and formatting uses oxfmt (config: .oxfmtrc.json, scoped to TypeScript sources). Both honour .gitignore.

License

MIT