EG
Resume
·frontend·
#react#next.js#performance#rsc

finally wrapping my head around React Server Components and honestly it's less about new syntax and more about a completely different mental model.

The Old Mental Model

everything runs on client → fetch data → render → interactivity

'use client';

const [data, setData] = useState([]);

useEffect(() => {
  fetch('/api/data').then(res => res.json()).then(setData);
}, []);

return <DataList data={data} />;

problems: loading states, client-side waterfall, large JS bundle, potential race conditions.

The RSC Mental Model

components can run on server → fetch data there → send HTML to client → add interactivity only where needed

// this runs on server - no client JS needed
async function DataList() {
  const data = await db.query('SELECT * FROM products');
  return <ProductGrid products={data} />;
}

no useEffect. no loading state. no client-side data fetching. just... data and JSX.

The Key Insight

think about where the code runs, not just what it renders.

| Concern | Server Components | Client Components | |---------|-------------------|-------------------| | Data fetching | directly from DB | via API | | Bundle size | 0 KB | their JS size | | Interactivity | none (static) | event handlers, state | | Access to browser | none | full access |

When to Use What

Server Component (default):

// page.tsx - fetch data, render UI
async function Page() {
  const data = await getData();
  return <Layout>{data.map(item => <Item key={item.id} {...item} />)}</Layout>;
}

Client Component (only when needed):

'use client';

// search.tsx - needs interactivity
'use client';
const [query, setQuery] = useState('');

return (
  <input
    value={query}
    onChange={e => setQuery(e.target.value)}
    placeholder="Search..."
  />
);

The Pattern I Use

  1. Start with Server Components
  2. Only add 'use client' when:
    • Need useState/useEffect
    • Need browser APIs
    • Need event handlers
    • Using client-side libraries

Common Mistakes

❌ Making entire page 'use client' ❌ Fetching data in Client Component when you could in Server ❌ Not understanding client vs server boundary

✅ Keep client boundaries small ✅ Push data fetching to server ✅ Add interactivity only where needed

Real Example

// page.tsx (server) - fetch user data
async function ProfilePage({ params }) {
  const user = await getUser(params.id);
  return (
    <div>
      <ProfileHeader user={user} /> {/* server */}
      <FollowButton userId={user.id} /> {/* client - needs onClick */}
    </div>
  );
}

// FollowButton.tsx (client) - only interactive part
'use client';
export function FollowButton({ userId }) {
  const [following, setFollowing] = useState(false);
  
  return (
    <button onClick={() => setFollowing(!following)}>
      {following ? 'Following' : 'Follow'}
    </button>
  );
}

Benefits I've Noticed

  • way smaller JS bundles
  • no loading spinners for initial data
  • SEO actually works (content is HTML)
  • feels faster to users

The Learning Curve

ngl, initially confusing. took me a few projects to get comfortable. but once it clicks, you can't go back.

resources that helped:

  • Next.js docs (actually good)
  • Building a real project with it
  • thinking in server/client boundaries

default to server. add client only when needed. this single rule changed how I architect apps.

Related Articles