Fix Chat Crash and Build Error#343
Conversation
Refactors the ChatPanel's handleSubmit function to prevent a race condition that caused a 'Rendered more hooks than during the previous render' error. The previous implementation updated the UI state in two separate, asynchronous steps. This created a window where the component tree could change between renders, leading to a hook mismatch and a crash when a user submitted a new message while a previous one was still being processed. The new implementation ensures atomicity by updating the UI state with both the user's message and the initial, streamable assistant response in a single, synchronous operation. This guarantees a consistent component structure across renders, eliminating the crash and enabling a stable, concurrent chat experience. Also fixes a build error in `mapbox_mcp/hooks.ts` by correcting an invalid import path.
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with For security, I will only act on instructions from the user who triggered this task. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
💡 Enable Vercel Agent with $100 free credit for automated AI reviews |
|
|
|
Important Review skippedBot user detected. To trigger a single review, invoke the You can disable this status message by setting the WalkthroughThe chat panel's form submission handler transitions from asynchronous to synchronous operation with trimmed input validation. User content is built as a composite array containing text and image references before submission. FormData preparation occurs before clearing fields, and server responses are consolidated into a single UI state update alongside the user message. Changes
Sequence DiagramsequenceDiagram
participant User
participant Form as Chat Form
participant Handler as Submit Handler
participant Server as Server
participant UI as UI State
User->>Form: Enter message + attach file
User->>Form: Click submit
Form->>Handler: Trigger submission
Handler->>Handler: Trim input
alt Input empty & no file
Handler->>UI: Early return
else Valid input or file
Handler->>Handler: Build content array<br/>(text + image ref)
Handler->>Handler: Create FormData<br/>+ append file
Handler->>Handler: Clear input & attachment
Handler->>Server: submit(formData)<br/>[no await]
Server->>Server: Process request
Server-->>Handler: Return response
Handler->>UI: setMessages([<br/>UserMessage,<br/>ResponseMessage<br/>])
UI->>User: Display updated chat
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes The chat-panel changes introduce meaningful logic modifications—transitioning handler patterns, adding input validation, building composite content structures, and refactoring state management—yet remain localized to a single file with clear intent. The import path change is trivial. Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
Comment |
PR Compliance Guide 🔍Below is a summary of compliance checks for this PR:
Compliance status legend🟢 - Fully Compliant🟡 - Partial Compliant 🔴 - Not Compliant ⚪ - Requires Further Human Verification 🏷️ - Compliance label |
||||||||||||||||||||||||
PR Code Suggestions ✨Explore these optional code suggestions:
|
||||||||||||
|
@coderabbitai there are no tokens being generated? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 8409 🏁 Script executed: Length of output: 20205 @ngoiyaeric I found the issue! The problem is that Here's the problematic code in // Call the server action. It will immediately return a streamable UI component.
const responseMessage = submit(formData); // ❌ Missing await - this is a Promise!
setMessages(currentMessages => [
...currentMessages,
{
id: nanoid(),
component: <UserMessage content={content} />,
},
responseMessage as any, // ❌ Adding Promise instead of the resolved value
]);Fix: Make const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!input.trim() && !selectedFile) {
return;
}
// Create the user message content first
const content: ({ type: 'text'; text: string } | { type: 'image'; image: string })[] = [];
if (input) {
content.push({ type: 'text', text: input });
}
if (selectedFile && selectedFile.type.startsWith('image/')) {
content.push({
type: 'image',
image: URL.createObjectURL(selectedFile)
});
}
// Prepare the form data
const formData = new FormData(e.currentTarget);
if (selectedFile) {
formData.append('file', selectedFile);
}
// Clear the input fields
setInput('');
clearAttachment();
// Await the server action to get the actual streamable UI component
const responseMessage = await submit(formData);
// Update UI with both messages atomically
setMessages(currentMessages => [
...currentMessages,
{
id: nanoid(),
component: <UserMessage content={content} />,
},
responseMessage as any,
]);
};The key change is:
This ensures you get the actual response object with the streamable component, not a Promise, which will allow tokens to generate properly. |
There was a problem hiding this comment.
- The new
handleSubmitrisks appending a Promise to state and causing unhandled rejections ifsubmit(formData)is async; use a placeholder-and-replace pattern to keep updates atomic and safe. as anyobscures the contract between the server action andmessages, reducing type safety; define and use a concrete message type.dev.logshould not be committed; add it to.gitignoreand remove it from the repo.- Consider revoking image object URLs to prevent memory leaks over time.
Additional notes (3)
-
Performance |
components/chat-panel.tsx:86-86
Creating an object URL for the image without revoking it can lead to memory leaks, especially if many images are uploaded over time. Consider revoking the URL once the image is no longer needed (e.g., after it loads or when the message component unmounts). -
Readability |
components/chat-panel.tsx:74-81
You useinput.trim()for the empty-check but then push the untrimmedinputintocontent. This can lead to inconsistent behavior (e.g., extra trailing spaces in the stored message). Use a single trimmed value for both validation and message content. -
Maintainability |
mapbox_mcp/hooks.ts:11-11
The relative import fixes the build, but it may be brittle if the file moves. If the project uses path aliases (e.g.,baseUrl/pathsintsconfig.json), prefer a stable alias import for consistency across the codebase.
Summary of changes
- Refactored
handleSubmitincomponents/chat-panel.tsxto perform a single atomicsetMessagesupdate that appends the user message and the initial assistant response together. - Adjusted empty-input guard to ignore whitespace-only submissions via
input.trim(). - Minor formatting updates (semicolons, comments) in
components/chat-panel.tsx. - Fixed invalid import path in
mapbox_mcp/hooks.tsfromQCX/lib/utilsto../lib/utils. - Added
dev.log(development runtime log) to the repository.
| const responseMessage = submit(formData); | ||
|
|
||
| // Update the UI state with both the user's message and the initial assistant response | ||
| // in a single operation. This ensures the component tree structure is consistent. | ||
| setMessages(currentMessages => [ | ||
| ...currentMessages, | ||
| { | ||
| id: nanoid(), | ||
| component: <UserMessage content={content} />, | ||
| }, | ||
| responseMessage as any, | ||
| ]); |
There was a problem hiding this comment.
This assumes submit(formData) returns a synchronous value. If submit is a Next.js Server Action (or otherwise async), this will create an unhandled Promise and also append a Promise-like object into state, which can cause runtime errors and non-deterministic behavior. It also means rejections become unhandled. You can still preserve atomic UI updates by inserting a placeholder assistant message in the same setMessages call as the user message, then resolve the server action and replace the placeholder when it completes.
Suggestion
Consider adding a placeholder assistant message atomically, then replacing it when the server action resolves. For example:
// Call the server action and immediately stage a placeholder assistant message
const pendingId = nanoid();
const userMsg = {
id: nanoid(),
component: <UserMessage content={content} />,
};
setMessages((curr) => [
...curr,
userMsg,
{ id: pendingId, component: <span aria-busy="true">Thinking…</span> },
]);
submit(formData)
.then((resolved) => {
setMessages((curr) =>
curr.map((m) => (m.id === pendingId ? (resolved as any) : m))
);
})
.catch((err) => {
// Optional: surface an error state in place of the placeholder
setMessages((curr) =>
curr.map((m) =>
m.id === pendingId
? { id: pendingId, component: <span role="alert">Something went wrong</span> }
: m
)
);
console.error('submit(formData) failed', err);
});Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.
| id: nanoid(), | ||
| component: <UserMessage content={content} />, | ||
| }, | ||
| responseMessage as any, |
There was a problem hiding this comment.
Relying on as any here hides the real contract between submit and your messages state shape. This makes it easier for future changes to introduce subtle UI bugs. Typing the submit return value (e.g., a ChatMessage) will let you remove as any and keep the code safer and self-documenting.
Suggestion
Define a concrete message type and type the server action accordingly, then remove the as any:
// Example
type ChatMessage = { id: string; component: React.ReactNode };
// In the server action typing
async function submit(formData: FormData): Promise<ChatMessage> { /* ... */ }
// When replacing the placeholder in the previous suggestion
.then((resolved: ChatMessage) => {
setMessages((curr) => curr.map((m) => (m.id === pendingId ? resolved : m)));
})Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.
| ▲ Next.js 15.5.4 (Turbopack) | ||
| - Local: http://localhost:3000 | ||
| - Network: http://192.168.0.2:3000 | ||
| - Environments: .env | ||
| - Experiments (use with caution): | ||
| · serverActions | ||
|
|
||
| ✓ Starting... | ||
| Attention: Next.js now collects completely anonymous telemetry regarding usage. | ||
| This information is used to shape Next.js' roadmap and prioritize features. | ||
| You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL: | ||
| https://nextjs.org/telemetry |
There was a problem hiding this comment.
A development runtime log (dev.log) is committed to the repository. This should not be versioned, as it adds noise and churn to the repo and can accidentally leak environment details.
Suggestion
Remove dev.log from the repo and ignore it going forward by adding it to .gitignore:
# development logs
dev.logThen delete the file from version control:
git rm --cached dev.logReply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
components/chat-panel.tsx (2)
111-111: Replaceas anywith a typed message contract.Type the server action return (e.g., ChatMessage) and remove the cast for safety and self-documentation.
Example:
- type ChatMessage = { id: string; component: React.ReactNode }
- async function submit(formData: FormData): Promise { … }
- Then: setMessages(curr => curr.map(m => (m.id === pendingId ? resolved : m)))
100-112: Blocker: submit(formData) is async; a Promise is being inserted into state, causing “no tokens” and potential unhandled rejections.Keep the UI tree shape atomic using a placeholder, but still await the server action before replacement.
Apply this diff to make handleSubmit async, stage a placeholder, then replace it on resolution (and surface errors):
- const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { + const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); if (!input.trim() && !selectedFile) { return; } // Create the user message content first, while we still have the input and file const content: ({ type: 'text'; text: string } | { type: 'image'; image: string })[] = []; if (input) { content.push({ type: 'text', text: input }); } if (selectedFile && selectedFile.type.startsWith('image/')) { content.push({ type: 'image', image: URL.createObjectURL(selectedFile) }); } // Prepare the form data for the server action const formData = new FormData(e.currentTarget); if (selectedFile) { formData.append('file', selectedFile); } // Clear the input fields for the user setInput(''); clearAttachment(); - // Call the server action. It will immediately return a streamable UI component. - const responseMessage = submit(formData); - - // Update the UI state with both the user's message and the initial assistant response - // in a single operation. This ensures the component tree structure is consistent. - setMessages(currentMessages => [ - ...currentMessages, - { - id: nanoid(), - component: <UserMessage content={content} />, - }, - responseMessage as any, - ]); - }; + const pendingId = nanoid(); + const userMsg = { + id: nanoid(), + component: <UserMessage content={content} />, + }; + + // Atomic insert of user + placeholder keeps hook order stable. + setMessages(curr => [ + ...curr, + userMsg, + { id: pendingId, component: <span aria-busy="true">Thinking…</span> }, + ]); + + try { + const resolved = await submit(formData); + setMessages(curr => + curr.map(m => (m.id === pendingId ? (resolved as any) : m)) + ); + } catch (err) { + console.error('submit(formData) failed', err); + setMessages(curr => + curr.map(m => + m.id === pendingId + ? { id: pendingId, component: <span role="alert">Something went wrong</span> } + : m + ) + ); + } + };This preserves the “atomic” tree shape while ensuring a real, streamable UI message replaces the placeholder when the server action resolves.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
⛔ Files ignored due to path filters (2)
bun.lockis excluded by!**/*.lockdev.logis excluded by!**/*.log
📒 Files selected for processing (2)
components/chat-panel.tsx(1 hunks)mapbox_mcp/hooks.ts(1 hunks)
🔇 Additional comments (1)
mapbox_mcp/hooks.ts (1)
11-11: Import path fix LGTM.The relative path change resolves typical module resolution/build errors for local utils.
Please run a type check/build to confirm no remaining path alias collisions in this package after the change.
| const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { | ||
| e.preventDefault(); | ||
| if (!input.trim() && !selectedFile) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
Trimmed validation vs send-button disabled state are inconsistent.
You guard on input.trim() here, but the submit button uses input.length (Line 254). Users can click “Send” with only spaces, then nothing happens. Align the disabled condition to trimmed length.
If you agree, update the button to: disabled={input.trim().length === 0 && !selectedFile}.
I'll verify the review comment by examining the actual code in the chat-panel.tsx file.
🏁 Script executed:
#!/bin/bash
# Find chat-panel.tsx and extract the relevant sections
fd "chat-panel.tsx" --type fLength of output: 25
🏁 Script executed:
#!/bin/bash
# Extract lines around 72-76 to see handleSubmit
if [ -f "components/chat-panel.tsx" ]; then
echo "=== Lines 70-80 (handleSubmit context) ==="
sed -n '70,80p' "components/chat-panel.tsx"
echo ""
echo "=== Lines 250-258 (submit button context) ==="
sed -n '250,258p' "components/chat-panel.tsx"
fiLength of output: 776
🏁 Script executed:
#!/bin/bash
# Search for all occurrences of disabled attribute on buttons to understand the full context
if [ -f "components/chat-panel.tsx" ]; then
echo "=== Searching for 'disabled=' pattern ==="
rg -n "disabled=" "components/chat-panel.tsx" -A 2 -B 2
fiLength of output: 232
🏁 Script executed:
#!/bin/bash
# Search for input.length and input.trim patterns
if [ -f "components/chat-panel.tsx" ]; then
echo "=== Searching for input.length or input.trim patterns ==="
rg -n "input\.(length|trim)" "components/chat-panel.tsx" -B 1 -A 1
fiLength of output: 409
Fix button disabled state to prevent clicking with only whitespace input.
Line 254's disabled condition uses input.length === 0, but handleSubmit (line 74) validates !input.trim(). This allows users to click "Send" with only spaces, where nothing happens. Also, line 228 validates input.trim().length === 0, indicating consistent intent across handlers.
Update line 254 to: disabled={input.trim().length === 0 && !selectedFile}
🤖 Prompt for AI Agents
In components/chat-panel.tsx around lines 72-76 (handleSubmit) and where the
Send button is rendered (line ~254), the submit handler uses input.trim() but
the button disabled check uses input.length === 0, allowing whitespace-only
clicks; update the button disabled expression to use trimmed input so it matches
validation (disabled={input.trim().length === 0 && !selectedFile}), ensuring the
Send button is disabled when input is only whitespace and no file is selected.
| content.push({ | ||
| type: 'image', | ||
| image: URL.createObjectURL(selectedFile) | ||
| }) | ||
| }); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Potential object URL leak for image previews.
URL.createObjectURL needs revocation. Ensure the component that renders the preview calls URL.revokeObjectURL on unmount or after the image loads, or generate the object URL within and manage cleanup there.
Would you like a small PR to move object URL creation into UserMessage with a useEffect cleanup?
🤖 Prompt for AI Agents
In components/chat-panel.tsx around lines 84-88, the code calls
URL.createObjectURL(selectedFile) to preview an image but never revokes the
object URL, causing a memory leak; fix by moving object URL creation into the
UserMessage component (or otherwise into a component with lifecycle) and manage
cleanup: create the URL in a useEffect, set it to local state, revoke it in the
effect cleanup and also revoke it after the image load if desired, and ensure
any previously created URL is revoked when the file/message changes or when the
component unmounts.
@coderabbitai can you get me an agent prompt for this |
|
Tip For best results, initiate chat on the files or code changes. @ngoiyaeric Here's an agent prompt you can use with @google-labs-jules to fix the token generation issue: Agent Prompt: This prompt clearly identifies the file, the problem, and the exact changes needed. You can provide this to Jules to implement the fix. |
Refactors the UserMessage component to properly manage the lifecycle of object URLs created for image previews. Previously, `URL.createObjectURL` was called in the ChatPanel component, but the resulting URL was never revoked, leading to a memory leak in the browser. This change moves the responsibility of creating and revoking the object URL into the UserMessage component. A `useEffect` hook now creates the URL when the component mounts and revokes it in the cleanup function when the component unmounts, ensuring that the memory is released correctly.
User description
This change fixes a critical React error (#310) that occurred when users performed actions, such as sending a new chat message, while a previous asynchronous operation was already in progress. The root cause was a race condition in the
ChatPanelcomponent where the UI state was being updated from multiple sources in a non-atomic way, leading to an inconsistent number of hooks between renders.The solution refactors the
handleSubmitfunction incomponents/chat-panel.tsxto ensure that UI state updates are atomic. It now adds both the user's message and the initial, streamable response from the Vercel AI SDK to the UI in a single, coordinated state update. This prevents the hook mismatch error and stabilizes the application, allowing for a more responsive and concurrent user experience.Additionally, this change fixes a build error caused by an invalid import path in
mapbox_mcp/hooks.ts.PR created automatically by Jules for task 16798323145761769371
PR Type
Bug fix
Description
Fixes race condition in
ChatPanelcausing hook mismatch crashhandleSubmitto update UI state atomically with both user message and assistant response in single operationCorrects invalid import path in
mapbox_mcp/hooks.tsDiagram Walkthrough
File Walkthrough
hooks.ts
Fix invalid import pathmapbox_mcp/hooks.ts
QCX/lib/utilsto../lib/utilschat-panel.tsx
Prevent race condition in chat submissioncomponents/chat-panel.tsx
handleSubmitfrom async to synchronous functionconditions
setMessagescallinput.trim()for better empty checkSummary by CodeRabbit
Improvements
Chores