It feels strange to call React "Legacy," but here we are. Between 2016 and 2022, the industry standard for building web apps was Create React App (CRA).
useEffect.There are tens of thousands of these apps in the wild. They power massive SaaS platforms. And they are starting to show their age.
The industry has moved to React Server Components (RSC) via the Next.js App Router. This isn't just an upgrade; it is a fundamental architectural shift. Migrating is necessary, but it is hard.
The biggest barrier to migration isn't syntax; it's the mental model.
The Old World (Client-Side) Thinking in "Time": When does this run?
useEffect -> Update State -> Re-render."
The browser does all the work.The New World (Server-Side) Thinking in "Place": Where does this run?
We never recommend a "Big Bang" rewrite (stopping feature dev to rewrite the whole app). It notoriously leads to failed projects. Instead, we use the Strangler Fig Pattern. We slowly wrap the old app in the new architecture until the old app disappears.
We create a Next.js App Router project alongside the existing CRA app.
We use Next.js Rewrites to serve the old app on a catch-all route (/[...slug]).
We migrate the "Shell"—the Sidebar, Header, and Footer—to Next.js layouts.
The "Page Content" is still the legacy app, loaded via an iframe or a shared component mount.
Now the user gets the speed benefit of a static shell, even if the inner content is still checking useEffect.
We pick a high-value page. Let's say, the "Dashboard Landing." We rewrite it using Server Components.
useEffect: We delete client-side fetching hooks.Example Code Change:
Legacy (Client)
Modern (Server)
We push the "client" logic down to the leaves of the tree.
The "Submit Button," the "Chart," and the "Drag and Drop" zone become 'use client' components. The parent page remains a Server Component.
This drastically reduces the JavaScript bundle size.
Why go through this pain?
date-fns or internal logic) on the server, we don't send them to the user's browser.isLoading states for every single data fetch. The code is linear and easier to read.The future of React is Server-First. The migration is inevitable. If you have a large legacy codebase and don't know where to start, looking into our Services might be the first step. We have effectively migrated enterprise platforms without downtime.
Don't let tech debt slow you down. Modernize incrementally.
Found this useful?
Share it with your network
function Dashboard() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetch('/api/stats').then(r => r.json()).then(d => { setData(d); setLoading(false); }); }, []); if (loading) return <Spinner />; return <Stats data={data} />;}async function Dashboard() { // Runs on server. Direct DB access or internal fetch const data = await getStats(); // HTML sent ready-to-render. No spinner. return <Stats data={data} />;}