Draft
Conversation
New rule (severity: warn) flagging the §6 anti-pattern from React's
'You Might Not Need an Effect' guide:
const [jsonToSubmit, setJsonToSubmit] = useState(null);
useEffect(() => {
if (jsonToSubmit !== null) {
post('/api/register', jsonToSubmit);
}
}, [jsonToSubmit]);
function handleSubmit(event) {
event.preventDefault();
setJsonToSubmit({ firstName, lastName });
}
The state variable exists only to schedule an effect to run on click.
The fix is to call `post('/api/register', { firstName, lastName })`
directly inside handleSubmit and delete the state — and that's exactly
what the rule's diagnostic recommends.
Detector pre-conditions (all four must hold) — chosen to keep
real-world false positives near zero:
(1) useEffect with a single-identifier dep array, where the dep is
a useState binding declared in this component
(2) effect body is exactly one IfStatement guarding on that state
with one of: bare truthy, !== null/undefined, === Literal,
.length, or !X
(3) IfStatement.consequent contains a CallExpression whose callee
is in EVENT_TRIGGERED_SIDE_EFFECT_CALLEES (fetch, post, navigate,
showNotification, alert, track, ...) OR a MemberExpression
whose property is in EVENT_TRIGGERED_SIDE_EFFECT_MEMBER_METHODS
(`axios.post`, `router.push`, `analytics.track`, etc.)
(4) every setStateX call site in the component is inside a JSX
`on*` handler (or a function bound to one) — i.e. the trigger
is set only by user interactions
(4) is the strongest signal that the state exists *only* to schedule
the effect, and is what distinguishes this rule from §5 (handled by
the existing no-effect-event-handler).
Reuses existing helpers `collectHandlerBindingNames` /
`isInsideEventHandler` from the same file.
Tests: 7 regression cases.
flags:
- canonical post-trigger shape
- axios.post member-call shape
- bare truthy guard with navigate(...)
does NOT flag:
- article's GOOD analytics-on-mount example (empty deps, no trigger)
- state also written outside handlers (mixed reactive logic)
- guard with a non-side-effect callee (compute(seed))
- guard on a prop (no useState binding present)
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
New rule (severity:
warn) flagging the §6 anti-pattern from React's You Might Not Need an Effect guide.What it catches
The state variable exists only to schedule an effect to run on click. The fix is to call
post(...)directly insidehandleSubmitand delete the state.Detector — four pre-conditions, all must hold
Chosen to keep real-world false positives near zero:
useEffectwith a single-identifier dep array, where the dep is auseStatebinding declared in this component.IfStatementguarding on that state with one of:if (X)!== null/!== undefined/!= null=== LiteralX.length(truthy)!X(negated)if's consequent contains aCallExpressionwhose callee is in a small allowlist:fetch,post,put,patch,del,ky,got,wretch,ofetch,navigate,navigateTo,showNotification,toast,alert,confirm,track,logEvent,logVisit,captureEventpost,put,patch,delete,push,replace,navigate,capture,track,logEvent— coversaxios.post,router.push,analytics.track,posthog.capture, etc.setX(...)call site in the component is inside a JSXon*handler (or a function bound to one) — i.e. the trigger is set only by user interactions.(4) is the strongest signal that the state exists only to schedule the effect, and is what distinguishes this rule from §5 (handled by the existing
no-effect-event-handler).What it does NOT flag
The article's legitimate analytics-on-mount effect:
is not flagged — empty deps, no trigger state, runs because the form was displayed.
State that's also written by other reactive logic (another effect, top-of-render adjustment) isn't flagged either — that's a different pattern.
Tests — 7 regression cases
post-trigger shapeaxios.postmember-call shapenavigate(...)compute(seed))useStatebinding)Plus a smoke test in
run-oxlint.test.tsagainst a fixture component (EventTriggerStateComponent).Reuse
Reuses existing
collectHandlerBindingNamesandisInsideEventHandlerhelpers already in the file (used byrerender-defer-reads-hook).Checks
488/488 tests passing locally. Lint, typecheck, format clean. Changeset included (minor bump).