back to the home page One Year with Next.js App Router — Why We're Moving On A critique of React Server Components and Next.js 15. webdev technical analysis opinion
As I've been using Next.js professionally on my employer's web app, I find the core design of their App Router and React Server Components (RSC) to be extremely frustrating. And it's not small bugs or that the API is confusing, but large disagreements about the fundamental design decisions that Vercel and the React team made when building it.
The more webdev events I go to, the more I see people who dislike Next.js, but still get stuck using it. By the end of this article, I will share how me and my colleagues escaped this hell, seamlessly migrating our entire frontend to TanStack Start.
§A Technical Review, What are Server Components?
The pitch of RSC is that components are put into two categories, "server" components and "client" components. Server components don't have useState , useEffect , but can be async function s and refer to backend tools like directly calling into a database. Client components are the existing model, where there is code on the backend to generate HTML text and frontend code to manage the DOM using window.document.* .
The first disaster: naming!! React is now using the words "server" and "client" to refer to a very specific things, ignoring their existing definitions. This would be fine, except Client components can run on the backend too! In this article, I'll be using the terms "backend" and "frontend" to describe the two execution environments that web apps exist in: a Node.js process and a Web browser, respectively.
This Server/Client component model is interesting. Since built-ins like
src/app/[username]/page.tsx export default async function Page ({ params }) { const { username } = await params ; return < main > < UserInfo username = { username } /> < Suspense fallback = {< PostListSkeleton />} > < UserPostList username = { username } /> Suspense > main > } async function UserInfo ({ username }) { const user = await fetchUserInfo ( username ); return <> < h1 > { user . displayName } h1 > { user . bio ? < Markdown content = { user . bio } /> : "" } > } async function UserPostList ({ username }) { const posts = await fetchUserPostList ( username ); return ; }
If we ignore the 40kB gzipped bundle size of React itself, the above example has zero JavaScript for the UI and data fetching — it just streams the markup! For example, the imagined markdown parser within the
src/components/CopyButton.tsx "use client" ; export function CopyButton ({ url }) { return <> < span > { url } span > < button onClick = { () => { const full = new URL ( url , location .href); navigator. clipboard . writeText ( full .href); }} > copy button > > }
... continue reading