react-doctor/activity-wraps-effect-heavy-subtree
Audit the `<Activity>` subtree: every hide/show cycle tears down and recreates every `useEffect`/`useLayoutEffect` inside, so move subscriptions and effect-driven setState chains outside the boundary or pre-resolve the data above it.
- Category: Bugs
- Severity: warn
- Source:
oxlint-plugin-react-doctor - Framework: global
- Enabled when: always
- Default: Enabled
- Documentation: https://react.dev/reference/react/Activity
Validation prompt
Use this to decide whether a fired diagnostic is real or a false positive.
Fires only on React 19.2+ when a JSX element resolving to React's Activity/unstable_Activity (named import, alias like unstable_Activity as UA, or <React.Activity> where the namespace object is React's default import or * as React binding) has a mode prop whose value is a NON-static, toggleable expression such as mode={open ? "visible" : "hidden"}, AND it wraps at least one capitalized same-file child component (collected from JSXOpeningElement JSXIdentifier names, excluding the locally-bound Activity name) whose function body contains one or more useEffect/useLayoutEffect calls. False positive — suppress when: (1) mode is a static literal (mode="hidden", mode="visible", mode={"hidden"}) or absent (default visible), since a pinned boundary has no hide/show cycle; (2) the wrapped child is a member-expression tag like <Charts.Bar />, which is skipped during child collection (the trailing identifier is never looked up) even when a same-file Bar helper with effects exists, because such children resolve through an external namespace; (3) the boundary element ITSELF is a member-expression like <Calendar.Activity> whose namespace object (import * as Calendar from "./calendar") is not React's default/* as React binding — the detector rejects it at boundary detection so it never fires even though its same-file <Entry /> child is effectful; (4) the wrapped component is imported from another module (v1 is same-file only); (5) the children have no effect hooks; or (6) the tag is a user component named Activity imported from a non-react module. Toggling the effects intentionally on hide/show (a subscription that genuinely should pause while hidden) is benign-by-design and should be suppressed.
Fix prompt
Use this once validation confirms the diagnostic is real.
Decide whether the inner Effects belong inside the Activity boundary at all: a toggleable <Activity mode={open ? "visible" : "hidden"}> tears down and recreates every wrapped useEffect/useLayoutEffect on each hide/show, causing a remount storm on dense screens. Lift subscriptions, observers, and effect-driven setState chains into a parent that stays mounted across the toggle, or pre-resolve the data above the boundary and pass it down as props so the hidden subtree holds state without re-running effects; reserve effects that truly must restart on show inside the boundary. Alternatively pin the mode statically if the subtree never actually toggles. See https://react.dev/reference/react/Activity
Related rules
More Bugs rules from the rules reference:
react-doctor/button-has-type: Set type="button" (or "submit"/"reset") explicitly on every <button> so it never defaults to submit.react-doctor/checked-requires-onchange-or-readonly: Pair `checked` with `onChange={…}` (controlled) or `readOnly` (display-only), and never combine `checked` with `defaultChecked`.react-doctor/client-localstorage-no-version: Bake a version into the storage key (e.g. "myKey:v1"); a future schema change can ignore old data instead of crashing on itreact-doctor/effect-listener-cleanup-mismatch: Effect cleanup does not match its event listenerreact-doctor/effect-needs-cleanup: Return a cleanup function that releases the subscription / timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` / `clearTimeout(id)` for timers, or `return unsubscribe` if the subscribe call already returned one