Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/react-doctor/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,7 @@ export const JSX_OPENER_SCAN_MAX_LINES = 32;
// HACK: lookback cap for stacked / near-miss disable-next-line scanning.
// Larger gaps stop being intentional suppressions and become noise.
export const SUPPRESSION_NEAR_MISS_MAX_LINES = 10;

// `useEffectEvent` requires React 19+. Below the threshold, the rule
// that suggests it (`prefer-use-effect-event`) stays silent.
export const USE_EFFECT_EVENT_MIN_MAJOR = 19;
8 changes: 7 additions & 1 deletion packages/react-doctor/src/oxlint-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { createRequire } from "node:module";
import { REACT_19_DEPRECATION_MIN_MAJOR, REACT_DOM_LEGACY_API_MIN_MAJOR } from "./constants.js";
import {
REACT_19_DEPRECATION_MIN_MAJOR,
REACT_DOM_LEGACY_API_MIN_MAJOR,
USE_EFFECT_EVENT_MIN_MAJOR,
} from "./constants.js";
import type { Framework } from "./types.js";

const esmRequire = createRequire(import.meta.url);
Expand Down Expand Up @@ -237,6 +241,7 @@ export const GLOBAL_REACT_DOCTOR_RULES: Record<string, RuleSeverity> = {
"react-doctor/no-derived-useState": "warn",
"react-doctor/no-direct-state-mutation": "warn",
"react-doctor/no-set-state-in-render": "warn",
"react-doctor/prefer-use-effect-event": "warn",
Comment thread
cursor[bot] marked this conversation as resolved.
"react-doctor/prefer-useReducer": "warn",
"react-doctor/rerender-lazy-state-init": "warn",
"react-doctor/rerender-functional-setstate": "warn",
Expand Down Expand Up @@ -376,6 +381,7 @@ const VERSION_GATED_RULE_IDS: ReadonlyMap<string, number> = new Map([
["react-doctor/no-react19-deprecated-apis", REACT_19_DEPRECATION_MIN_MAJOR],
["react-doctor/no-default-props", REACT_19_DEPRECATION_MIN_MAJOR],
["react-doctor/no-react-dom-deprecated-apis", REACT_DOM_LEGACY_API_MIN_MAJOR],
["react-doctor/prefer-use-effect-event", USE_EFFECT_EVENT_MIN_MAJOR],
]);

const filterRulesByReactMajor = (
Expand Down
26 changes: 26 additions & 0 deletions packages/react-doctor/src/plugin/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,32 @@ export const MUTATING_ROUTE_SEGMENTS = new Set([

export const EFFECT_HOOK_NAMES = new Set(["useEffect", "useLayoutEffect"]);
export const HOOKS_WITH_DEPS = new Set(["useEffect", "useLayoutEffect", "useMemo", "useCallback"]);

// Direct CallExpression callees whose function argument is a "sub-handler"
// β€” code that runs asynchronously, in response to an event the React render
// can't observe. Calling a reactive value from inside a sub-handler is the
// classic case for `useEffectEvent` (see "Separating Events from Effects").
export const SUB_HANDLER_DIRECT_CALLEE_NAMES = new Set([
"setTimeout",
"setInterval",
"requestAnimationFrame",
"requestIdleCallback",
"queueMicrotask",
]);

// MemberExpression-callee property names whose function arguments are
// sub-handlers. Same set as `SUBSCRIPTION_METHOD_NAMES` from the
// `prefer-use-sync-external-store` family β€” kept as a sibling constant
// rather than a re-export so the two rules can evolve independently.
export const SUB_HANDLER_MEMBER_METHOD_NAMES = new Set([
"subscribe",
"addEventListener",
"addListener",
"on",
"watch",
"listen",
"sub",
]);
export const CHAINABLE_ITERATION_METHODS = new Set(["map", "filter", "forEach", "flatMap"]);
export const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);

Expand Down
2 changes: 2 additions & 0 deletions packages/react-doctor/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ import {
noFetchInEffect,
noPropCallbackInEffect,
noSetStateInRender,
preferUseEffectEvent,
preferUseReducer,
rerenderDependencies,
rerenderDeferReadsHook,
Expand All @@ -207,6 +208,7 @@ const plugin: RulePlugin = {
"no-derived-useState": noDerivedUseState,
"no-direct-state-mutation": noDirectStateMutation,
"no-set-state-in-render": noSetStateInRender,
"prefer-use-effect-event": preferUseEffectEvent,
"prefer-useReducer": preferUseReducer,
"rerender-lazy-state-init": rerenderLazyStateInit,
"rerender-functional-setstate": rerenderFunctionalSetstate,
Expand Down
Loading