ReputationProfileView is marked "use client" but has no client-side behaviour, forcing a static profile page into the client bundle

Open
#847 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
54/100
Issue type
Refactor
Clarity
Mostly clear
Activity status
Active

Research direction

Start with src/app/reputation/[handle]/ReputationProfileView.tsx and inspect its imports, then read the parent page.tsx and the Avatar component to understand the server and client boundaries. Remove the directive and build the route to confirm the page still renders; the issue also suggests checking two sibling components and adding a regression assertion, which expands the scope. Done means the profile view renders as a server component while Avatar still works.

Written by the indexing model from the issue text.

Description

architecture performance

Location

src/app/reputation/[handle]/ReputationProfileView.tsx:1

Problem

The file opens with "use client", but the component is pure presentation over its single profile prop. Grepping the whole 115-line file for any client-only feature returns nothing:

grep -n 'use[A-Z]\|onClick\|onChange\|onSubmit\|window\|document' \
  src/app/reputation/\[handle\]/ReputationProfileView.tsx
# (no matches)

No hooks, no event handlers, no browser globals, no state. It renders an Avatar, a <h1>, Badges, four StatCards, Cards, an EmptyState and some Links — and the only one of those that genuinely needs the client is Avatar (src/components/ui/Avatar.tsx:1 is itself "use client"). StatCard, Card, Badge and EmptyState are all plain server-compatible modules with no "use client" directive of their own.

Because the directive sits at the top of this component, it marks the whole subtree as the client boundary: StatCard, Card, Badge, EmptyState, their lucide-react icon imports and the formatHours/messages modules they pull in are all compiled into the route's client bundle and re-executed during hydration, even though their output never changes after the server render.

The parent, src/app/reputation/[handle]/page.tsx, is already an async Server Component that awaits fetchReputationByUsername and hands the resolved profile down as a plain serialisable prop — so nothing about the data flow requires a client component here.

Why it matters

/reputation/[handle] is a public, indexable, read-only page — closed issue #19 and src/lib/seo-policy.ts treat it as a crawlable profile surface, and it is listed in the sitemap. It is the one route in the app with the strongest case for shipping as pure server-rendered HTML, and it currently ships a client bundle for markup that cannot change. The cost is paid on every profile view: extra JavaScript to download, parse and hydrate, for zero interactivity.

This is the same category of waste as closed issue #223 ("RootLayout wraps every route in client-side AuthProvider and WalletProvider, forcing full client hydration even for fully static, anonymous marketing pages") — a boundary drawn higher than the behaviour that needs it.

Reproduction

  1. npm run build and read the route summary — /reputation/[handle]'s First Load JS includes StatCard, Card, Badge, EmptyState and their icon imports.
  2. npm start, open a profile page, and in DevTools → Network filter to JS: the route's client chunk contains the stat-card and badge code.
  3. Confirm nothing is lost without it: delete line 1 and rebuild — the page renders identically, because no part of the component depends on the client.

Suggested fix

Remove the "use client" directive from src/app/reputation/[handle]/ReputationProfileView.tsx and let the component render on the server. Avatar already carries its own "use client", so it stays a client component and keeps working as a leaf inside a server parent — that is exactly the boundary this page wants.

While doing it, check the two sibling cases for the same pattern:

  • src/app/milestones/MilestoneFundingProgress.tsx — if it has no hooks or handlers either, it can be a server component too, with only MilestoneActions (which does have handlers) staying on the client.
  • src/app/dashboard/maintainer/PipelineBoard.tsx — currently a client component; if its only client need is Link, the boundary can move down.

Then add an assertion to ReputationProfileView.test.tsx (or a small lint rule) that the file has no "use client" directive, so the boundary does not drift back up the tree when the next prop is added.

Dominant language
TypeScript
Stars
1
Forks
94
Avg merge
1h 2m
Merged PRs (30d)
25

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from MergeFi/frontend

All issues in MergeFi/frontend

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.