How React Server Components Changed the Framework Landscape
TL;DR
RSC didn't just add a feature — it rewrote the rules for React framework design. When Next.js 13 shipped the app router with RSC, it forced every competing framework to respond. Remix rebuilt its data model (v3 with RSC support). Create React App was deprecated. SvelteKit and Nuxt doubled down on their own server models as RSC alternatives. The npm ecosystem split into "RSC-compatible" and "client-only" categories. Three years in, RSC is the default model for new React apps — but the transition is messier than anyone predicted.
Key Takeaways
- Next.js 15 — RSC default, pages router deprecated (not removed), ~60% of new Next.js projects use app router
- Remix v3 — Adopted RSC, merged with React Router v7
- Create React App — Officially deprecated in 2024; Vite + React is the replacement
- npm ecosystem — ~30% of React libraries needed RSC updates in 2024-2025
- The "use client" split — UI libraries now declare RSC boundaries explicitly
Before RSC: The Client-Side Fetching Era
// The pre-RSC pattern (still dominant in 2023)
// Everything is client-side: fetch in useEffect, loading states everywhere
'use client'; // Everything was implicitly this
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
function PackagePage() {
const { name } = useParams();
const [pkg, setPkg] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(`/api/packages/${name}`)
.then(r => r.json())
.then(data => { setPkg(data); setLoading(false); })
.catch(err => { setError(err); setLoading(false); });
}, [name]);
if (loading) return <Spinner />;
if (error) return <ErrorView error={error} />;
return <PackageDetails pkg={pkg} />;
}
// Problems:
// 1. Loading state is boilerplate
// 2. API route needed even for same-server data
// 3. Race conditions on fast navigation
// 4. No streaming — all or nothing
// 5. Component bundle sent to browser even for server-only logic
After RSC: The Server-First Era
// RSC pattern — server component does the heavy lifting
// This runs on the server. No useEffect, no loading state, no API route needed.
import { db } from '@/lib/db';
import { packages } from '@/lib/schema';
import { eq } from 'drizzle-orm';
import { Suspense } from 'react';
// No 'use client' = server component
export default async function PackagePage({ params }: { params: { name: string } }) {
// Direct DB query — no API layer needed
const pkg = await db.query.packages.findFirst({
where: eq(packages.name, params.name),
with: { dependencies: true, maintainers: true },
});
if (!pkg) {
notFound(); // Shows Next.js 404 page
}
return (
<div>
{/* Static parts render immediately */}
<PackageHeader pkg={pkg} />
<PackageDescription pkg={pkg} />
{/* Slow parts stream in later */}
<Suspense fallback={<DownloadChartSkeleton />}>
<DownloadChart packageName={pkg.name} /> {/* Another server component */}
</Suspense>
{/* Client component for interactivity */}
<CompareButton packageName={pkg.name} /> {/* 'use client' */}
</div>
);
}
How Frameworks Responded
Next.js: RSC as the Default
Next.js bet everything on RSC. The app router (released Next.js 13) was a full reimplementation:
Pages Router (legacy): App Router (RSC-default):
├── pages/ ├── app/
│ ├── index.tsx │ ├── page.tsx (server component)
│ ├── [slug].tsx │ ├── layout.tsx (server component)
│ └── api/ │ ├── loading.tsx (Suspense boundary)
│ └── route.ts │ ├── error.tsx (error boundary)
│ ├── not-found.tsx
│ └── [slug]/
│ └── page.tsx
The migration created one of the longest "should I migrate?" debates in React history. By 2026:
- ~60% of new Next.js projects use app router
- ~40% of existing Next.js projects still on pages router
- No deadline for pages router deprecation (but feature development paused)
Remix v3: The RSC Convergence
Remix v3 (merged with React Router v7) adopted RSC while keeping its "loaders and actions" mental model:
// Remix v3 — loaders remain, but can use RSC
// app/routes/packages.$name.tsx
import { useLoaderData } from 'react-router';
import { db } from '~/lib/db';
// Loader still exists but now RSC-aware
export async function loader({ params }: LoaderArgs) {
const pkg = await db.packages.findByName(params.name);
return { pkg };
}
// Component uses loader data OR direct RSC
export default function PackagePage() {
const { pkg } = useLoaderData<typeof loader>();
return <PackageDetails pkg={pkg} />;
}
Create React App: Deprecated
The most symbolic casualty of RSC was CRA:
# The official recommendation changed (React docs, 2024):
# ❌ Old: npx create-react-app my-app
# ✅ New: npm create vite@latest my-app -- --template react-ts
# OR: npx create-next-app@latest (if you need SSR)
CRA used Webpack + Babel, couldn't support RSC, and had slow builds. The React team officially deprecated it in 2024.
The npm Ecosystem Split
RSC created a new compatibility question every package author had to answer: "Does my package work in server components?"
Packages That Broke
// CSS-in-JS (styled-components, Emotion)
// ❌ Uses createContext, needs runtime injection
import styled from 'styled-components'; // Can't use in server components
// Browser API packages
// ❌ localStorage, window, navigator — server has none of these
import { useLocalStorage } from 'react-use'; // 'use client' only
Packages That Adapted
// next-intl — rewrote from scratch for RSC
import { getTranslations } from 'next-intl/server'; // Server component version
async function PackagePage() {
const t = await getTranslations('packages'); // Works in RSC
return <h1>{t('title')}</h1>;
}
// vs client component version:
'use client';
import { useTranslations } from 'next-intl';
function PackageCard() {
const t = useTranslations('packages');
return <span>{t('downloads')}</span>;
}
The New Pattern: Dual Exports
Smart libraries now ship both server and client APIs:
// next-auth v5 (Auth.js) — dual export pattern
import { auth } from '@/auth'; // Server component
import { useSession } from 'next-auth/react'; // 'use client'
// Server component
async function UserGreeting() {
const session = await auth(); // Direct session access
return <div>Hello {session?.user?.name}</div>;
}
// Client component
'use client';
function UserMenu() {
const { data: session } = useSession(); // Hook
return <div>{session?.user?.name}</div>;
}
The Remaining Debates
Three years after RSC launched, some things are still unsettled:
1. When to use server actions vs API routes
// Server action — form without API route
async function createPackage(formData: FormData) {
'use server';
await db.insert(packages).values({ name: formData.get('name') as string });
}
// vs API route — explicit, easier to test
// POST /api/packages → route.ts
Most teams default to API routes for anything called from JavaScript, and server actions for HTML form submissions.
2. Data fetching: RSC vs TanStack Query
- RSC for initial page load (SEO, fast first paint)
- TanStack Query for client-side updates, background refetch, optimistic updates
3. Where does global state live?
- Zustand/Jotai for UI state (still in
'use client'components) - RSC for fetching (replaces many TanStack Query
useQuerycalls) - The combo: RSC fetches → passes data to client components → TanStack Query manages mutations
Compare Next.js and Remix package health on PkgPulse.
See the live comparison
View nextjs vs. remix on PkgPulse →