Skip to content

fix(users): SSR issue#421

Merged
alexsapps merged 1 commit into
mainfrom
alex/users-ssr
May 29, 2026
Merged

fix(users): SSR issue#421
alexsapps merged 1 commit into
mainfrom
alex/users-ssr

Conversation

@alexsapps
Copy link
Copy Markdown
Collaborator

@alexsapps alexsapps commented May 29, 2026

Switch to useSuspenseQuery and add a in page.tsx

Hydration error was caused by user-form.tsx:176-190:

{isLoading && (
  <div className="flex items-center gap-2 text-muted-foreground text-sm">
    ...
  </div>
)}
{!isLoading && (
  <form ...>

Server renders the

(loading state). Client renders the .
React sees different DOM trees → hydration error.

Why they differ: During Next.js SSR of UserForm (a client component),
the Providers component (see providers.tsx:40-42) creates a fresh, empty
QueryClient on the server (isServer = true → makeQueryClient() on every
call). Even though HydrationBoundary is supposed to inject data into
that client via useMemo before UserForm renders, in practice the
TanStack Query hooks in UserForm see isLoading: true during the server
pass — the queries are in a pending/fetching state.

On the client side, HydrationBoundary successfully rehydrates the
singleton browser QueryClient with the prefetched data, so isLoading is
false and the form renders.

Summary by CodeRabbit

  • New Features

    • Added animated loading spinners on user edit and new user creation pages that display while forms initialize and load their data.
  • Improvements

    • Enhanced form submission behavior with improved interaction handling.
    • Optimized the user information form component with better data loading management for user details and chapter selections throughout the form lifecycle.

Review Change Stack

@alexsapps alexsapps requested a review from jakehobbs as a code owner May 29, 2026 06:35
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

User form components migrated to React Query Suspense API. UserForm refactored into UserFormInner (core form) and EditUserForm (edit fetch wrapper), with component using useSuspenseQuery for chapters and user data. Edit and new user pages wrapped with Suspense boundaries displaying animated spinner fallback.

Changes

User Form Suspense Migration

Layer / File(s) Summary
UserForm Suspense Refactoring
frontend-v2/src/app/(authed)/users/user-form.tsx
React Query import updated to useSuspenseQuery. Component restructured into UserFormInner (owned by mutation, form initialization), EditUserForm (edit-path user fetch via suspense), and exported UserForm (chapters fetch and delegation). Title always reflects user?.name, form submit wired to prevent default and call form.handleSubmit(), chapter selector simplified without loading-state conditionals.
Page Integration with Suspense Boundaries
frontend-v2/src/app/(authed)/users/[id]/page.tsx, frontend-v2/src/app/(authed)/users/new/page.tsx
Both pages import Suspense and Loader2 icon. UserForm wrapped in Suspense boundaries with animated loading fallback ("Loading..." with spinner), positioned inside existing HydrationBoundary.

Sequence Diagram

sequenceDiagram
  participant EditPage as Edit Page<br/>(Suspense boundary)
  participant UserForm as UserForm<br/>(coordinator)
  participant EditUserForm as EditUserForm<br/>(edit fetch)
  participant UserFormInner as UserFormInner<br/>(form & mutation)
  participant ReactQuery as React Query<br/>(useSuspenseQuery)
  
  EditPage->>UserForm: render with userId
  UserForm->>ReactQuery: useSuspenseQuery(chapters)
  ReactQuery-->>UserForm: chapters data
  UserForm->>EditUserForm: delegate with userId
  EditUserForm->>ReactQuery: useSuspenseQuery(user)
  ReactQuery-->>EditUserForm: user data
  EditUserForm->>UserFormInner: pass userId, user, chapters
  UserFormInner-->>EditPage: render form
  UserFormInner->>ReactQuery: useMutation(updateUser)
  ReactQuery-->>UserFormInner: mutation result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

A form takes shape in suspense's gentle flow,
Data fetched and rendered, no loading state to show.
Edit and create now share one inner heart,
Boundaries await the loading to start! 🐰✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'fix(users): SSR issue' is vague and does not clearly explain the specific solution implemented—wrapping components with Suspense boundaries to resolve hydration mismatches. Consider a more specific title like 'fix(users): resolve SSR hydration mismatch with Suspense boundaries' to better communicate the solution approach.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch alex/users-ssr

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
frontend-v2/src/app/(authed)/users/new/page.tsx (1)

29-38: 💤 Low value

Optional: extract the shared loading fallback.

This fallback block is duplicated verbatim in [id]/page.tsx. A small shared component (e.g. <FormLoadingFallback />) would keep the spinner/markup consistent across both pages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend-v2/src/app/`(authed)/users/new/page.tsx around lines 29 - 38,
Extract the duplicated fallback JSX into a new reusable component (e.g.,
FormLoadingFallback) that returns the spinner markup currently used as the
Suspense fallback; then replace the inline fallback in this file's Suspense
(surrounding UserForm) with <FormLoadingFallback /> and do the same replacement
in the other page that duplicates it (the [id]/page.tsx Suspense fallback) so
both pages share the same component and markup.
frontend-v2/src/app/(authed)/users/user-form.tsx (1)

45-45: ⚡ Quick win

Reuse the chapter list item shape instead of redefining Chapter locally.

frontend-v2/src/lib/api.ts already defines the getChapterList() return shape via ChapterListResp.parse(resp).chapters (items are { ChapterID: number; Name: string }), but it doesn’t currently export a Chapter type—only ChapterOrganizer is exported. Export an inferred Chapter type from lib/api (or derive it from getChapterList()’s return type) and reuse it in user-form.tsx (and other duplicates like user-table.tsx).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend-v2/src/app/`(authed)/users/user-form.tsx at line 45, Replace the
locally defined Chapter shape with a shared exported type from lib/api: add and
export an inferred Chapter type in lib/api (derive it from
ChapterListResp.parse(...).chapters or from getChapterList()'s return type) so
it lives alongside the existing ChapterOrganizer export, then import that
Chapter type into user-form.tsx and other duplicates (e.g., user-table.tsx) and
remove the local "type Chapter = { ChapterID: number; Name: string }"
definitions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend-v2/src/app/`(authed)/users/user-form.tsx:
- Around line 295-320: Add a segment-scoped error boundary for the Suspense
queries used in UserForm/EditUserForm so client errors from useSuspenseQuery
render a recoverable UI instead of bubbling to the global Next.js error handler:
either add a route-level error.tsx under the same app segment (e.g.,
app/(authed)/users/error.tsx) that exports the Next.js error boundary UI, or
wrap the components that call useSuspenseQuery (functions EditUserForm and
UserForm or their parent page) in an ErrorBoundary from react-error-boundary (or
a simple class-based React error boundary) and provide a fallback UI that lets
users retry or navigate away; ensure the boundary encloses both the Suspense and
the useSuspenseQuery calls so errors thrown in apiClient.getUser or
apiClient.getChapterList are caught.

---

Nitpick comments:
In `@frontend-v2/src/app/`(authed)/users/new/page.tsx:
- Around line 29-38: Extract the duplicated fallback JSX into a new reusable
component (e.g., FormLoadingFallback) that returns the spinner markup currently
used as the Suspense fallback; then replace the inline fallback in this file's
Suspense (surrounding UserForm) with <FormLoadingFallback /> and do the same
replacement in the other page that duplicates it (the [id]/page.tsx Suspense
fallback) so both pages share the same component and markup.

In `@frontend-v2/src/app/`(authed)/users/user-form.tsx:
- Line 45: Replace the locally defined Chapter shape with a shared exported type
from lib/api: add and export an inferred Chapter type in lib/api (derive it from
ChapterListResp.parse(...).chapters or from getChapterList()'s return type) so
it lives alongside the existing ChapterOrganizer export, then import that
Chapter type into user-form.tsx and other duplicates (e.g., user-table.tsx) and
remove the local "type Chapter = { ChapterID: number; Name: string }"
definitions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d8b1f9c9-4f85-4f58-8c95-0128e42293c7

📥 Commits

Reviewing files that changed from the base of the PR and between 902dfd9 and 279c655.

📒 Files selected for processing (3)
  • frontend-v2/src/app/(authed)/users/[id]/page.tsx
  • frontend-v2/src/app/(authed)/users/new/page.tsx
  • frontend-v2/src/app/(authed)/users/user-form.tsx

Comment thread frontend-v2/src/app/(authed)/users/user-form.tsx
Switch to useSuspenseQuery and add a <Suspense fallback={...}> in page.tsx

Hydration error was caused by user-form.tsx:176-190:

```
{isLoading && (
  <div className="flex items-center gap-2 text-muted-foreground text-sm">
    ...
  </div>
)}
{!isLoading && (
  <form ...>
```

Server renders the <div> (loading state). Client renders the <form>.
React sees different DOM trees → hydration error.

Why they differ: During Next.js SSR of UserForm (a client component),
the Providers component (see providers.tsx:40-42) creates a fresh, empty
QueryClient on the server (isServer = true → makeQueryClient() on every
call). Even though HydrationBoundary is supposed to inject data into
that client via useMemo before UserForm renders, in practice the
TanStack Query hooks in UserForm see isLoading: true during the server
pass — the queries are in a pending/fetching state.

On the client side, HydrationBoundary successfully rehydrates the
singleton browser QueryClient with the prefetched data, so isLoading is
false and the form renders.
@alexsapps alexsapps merged commit c1c8776 into main May 29, 2026
2 checks passed
@alexsapps alexsapps deleted the alex/users-ssr branch May 29, 2026 07:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant