fix: unconditional hook calls in ChatProfiles and FeedbackButtons#2912
Open
EyalAmitay wants to merge 3 commits into
Open
fix: unconditional hook calls in ChatProfiles and FeedbackButtons#2912EyalAmitay wants to merge 3 commits into
EyalAmitay wants to merge 3 commits into
Conversation
Both components violated React's Rules of Hooks by placing useEffect / useCallback calls after a conditional return null. When the guarding condition flips between renders (e.g. config.chatProfiles count changes as useConfig clears and refetches on chatProfile selection), React detects a different hook count and throws invariant 300/310. Fix: hoist all hook calls unconditionally above the guard, gate their side-effect logic on the same condition used by the early return. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jschwarz89
approved these changes
Apr 26, 2026
jschwarz89
left a comment
There was a problem hiding this comment.
Looks good to me, solves a real crash for us!
…les and FeedbackButtons Mounts each component then re-renders with the guarding condition flipped (config -> undefined -> config; showFeedbackButtons true -> false -> true) on the same fiber, asserting React doesn't log/throw a hooks-count mismatch. Verified by reverting f3a820d locally — both tests fail with "Rendered fewer hooks than expected" — and pass with the fix in place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
1 issue found across 4 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="frontend/tests/FeedbackButtons.spec.tsx">
<violation number="1" location="frontend/tests/FeedbackButtons.spec.tsx:86">
P2: The hook-order matcher misses React’s standard "Rendered fewer hooks than expected" error text, so this regression test can pass even when the hook-order bug is present.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
… messages React invariant 300 throws 'Rendered fewer hooks than expected' (not '…than during the previous render'), so the previous pattern missed it. Split into two separate patterns to cover both invariant 300 and 310. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ca58f2e to
11161d6
Compare
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.
Problem
ChatProfilesandFeedbackButtonsboth violate React's Rules of Hooks by callinguseEffect/useCallbackhooks after a conditionalreturn null. When the guarding condition changes between renders, the hook count differs and React throws invariant 300 ("Rendered fewer hooks than expected") or 310 ("Rendered more hooks than expected").ChatProfiles(lines 44–66)The copilot header renders
<ChatProfiles />wheneverconfig?.chatProfiles?.length >= 1. InsideChatProfiles, the early return fires whenlength <= 1— a different threshold — leaving 2useEffecthooks unreachable:How it fires in practice:
useConfigclearsconfigtoundefinedand refetches wheneverchatProfilechanges. When SWR has a cached response for the new URL it serves it synchronously, so React batchessetConfig(undefined)+setConfig(newData)into a single render —configtransitions directly from{chatProfiles:[A,B]}to{chatProfiles:[A]}without passing throughundefined. The parent header seeshasChatProfiles = !!(1) = trueand keepsChatProfilesmounted; the child hitslength <= 1and takes the early return after 7 hooks; React sees 9 hooks on render N and 7 on render N+1 → invariant 300. Whenconfigdoes pass throughundefined, the parent unmountsChatProfilesfirst and the fiber is destroyed before React can detect the mismatch — no error fires. The production flakiness reflects which code path the config update takes, not timing.FeedbackButtons(lines 50–96)Same pattern: 8
useState/useRecoilValuehooks beforeif (!showFeedbackButtons) return null, followed by 2useCallbackhooks.showFeedbackButtonsderives from!!config?.dataPersistence, so the same config-reload cycle can flip it.Evidence
~836 occurrences over 30 days across multiple production deployments, single error fingerprint
v11.04854025396C892A97427E50E80C58F4, all from*/copilot/index.js:Fix
Hoist all hook calls above the conditional early return; gate the side-effect logic on the same boolean as the guard.
Testing
Two regression tests added in
frontend/tests/:ChatProfiles.spec.tsx— mounts the component with 2 profiles, then re-renders withconfig = undefined(mirroringuseConfig'ssetConfig(undefined)on profile change), then re-renders with config restored. Asserts no"Rendered fewer/more hooks"error is logged. Fails on the unfixed code, passes after the fix.FeedbackButtons.spec.tsx— same pattern, togglingshowFeedbackButtonsbetweentrueandfalseon the same fiber.Before fix: both ChatProfiles.spec.tsx and FeedbackButtons.spec.tsx fail with "Rendered fewer hooks than expected. This may be caused by an accidental early return statement." - exactly React's invariant 300
message
After fix: all 34 tests pass
pnpm --filter @chainlit/app type-checkandpnpm lintboth pass.