New
Introducing React Bench, see how different models perform on React code

react-doctor/preact-prefer-oninput

Replace onChange with onInput on text-like inputs: onInput={(e) => setQuery(e.currentTarget.value)}

Validation prompt

Use this to decide whether a fired diagnostic is real or a false positive.

Fires on a JSXOpeningElement that is a <textarea>, or an <input> with no type attribute, or an <input> whose type string is not one of checkbox/radio/file (COMPAT_EXEMPT_INPUT_TYPES) — when that element also carries an onChange JSX attribute (so type="text", "password", "email", "number", and a dynamic type={expr} that the rule assumes is text-like all match). False positive: an element already using onInput rather than onChange, a <select> (only input and textarea match), and type="checkbox"/"radio"/"file" inputs whose native change event already fires correctly. Also suppress if preact/compat is loaded anywhere in the project — its renderer-level remap makes core onChange behave like onInput — though the engine already gates this via pure-preact (react present in deps means the rule sits out), so do not suppress merely because a given file imports only from preact/hooks.

Fix prompt

Use this once validation confirms the diagnostic is real.

In Preact core, onChange on text-like inputs only fires on blur, not per keystroke, so controlled inputs lag. Rename the attribute to onInput, keeping the same handler: change onChange={(e) => setQuery(e.currentTarget.value)} to onInput={(e) => setQuery(e.currentTarget.value)} on the <input>/<textarea>. Alternatively, if the project can adopt preact/compat, importing it remaps onChange automatically and no per-element change is needed. See https://preactjs.com/guide/v10/differences-to-react/#use-oninput-instead-of-onchange

More Bugs rules from the rules reference:

  • react-doctor/prefer-use-effect-event: Wrap the callback with `useEffectEvent(callback)` (React 19+) and call the resulting binding from inside the sub-handler. The Effect Event captures the latest props/state without being a reactive dep, so the effect doesn't re-subscribe on every parent render. See https://react.dev/reference/react/useEffectEvent
  • react-doctor/prefer-use-sync-external-store: Replace the `useState(getSnapshot())` + `useEffect(() => store.subscribe(() => setSnapshot(getSnapshot())))` pair with `useSyncExternalStore(store.subscribe, getSnapshot)`. The hook handles tearing during concurrent renders and SSR snapshots; the manual subscribe pattern doesn't
  • react-doctor/prefer-useReducer: Group related state: `const [state, dispatch] = useReducer(reducer, { field1, field2, ... })`
  • react-doctor/query-destructure-result: Destructure the query result instead of binding the whole object: const { data, isLoading } = useQuery(...)
  • react-doctor/query-mutation-missing-invalidation: Add `onSuccess: () => queryClient.invalidateQueries({ queryKey: ['...'] })` so cached data stays in sync after the mutation