Skip to content
Skip to content
Skip to content

Writing

What moved to the server, what moved back, and the one rule that survived.

to read
2 min
published
2026-05-08
filed under
React
A desk with a lamp and a notebook by a rainy city window at night

A year ago I moved a mid-sized product to React Server Components. Forty routes, a team of six, a deadline that did not move. This is the honest version of how it went.

What stayed on the server

Anything that reads data and does not respond to a finger: tables, detail pages, navigation, the first render of most forms. These got simpler. The data fetching sits next to the markup that uses it and there is no loading state to design, because the page arrives with the data in it.

tsx
// app/invoices/page.tsx: no client JavaScript at all
export default async function Invoices() {
  const rows = await db.invoices.recent(50)
  return (
    <Table>
      {rows.map((r) => <InvoiceRow key={r.id} invoice={r} />)}
    </Table>
  )
}

What moved back

Anything that has to answer in under fifty milliseconds. Filters, drag and drop, the command palette. We tried to keep the filters on the server for a month, with the URL as state. On a good connection it was fine. On a train it was not.

What surprised us

  • The bundle fell by 38%, mostly libraries that only ever formatted dates and money.
  • Caching became the hard problem. We wrote one page of rules and still broke them twice.
  • Tests got easier. A server component is a function that returns markup.

The rule that survived

A component belongs on the client only if the user can change it faster than the network can. Everything else is a document, and documents are what servers are for.

We wrote that sentence on the wall in month three and it has settled every argument since. Would I do the migration again? Yes, but in a quarter, not a sprint, and with the caching rules written first.

Writing

What moved to the server, what moved back, and the one rule that survived.

to read
2 min
published
2026-05-08
filed under
React
A desk with a lamp and a notebook by a rainy city window at night

A year ago I moved a mid-sized product to React Server Components. Forty routes, a team of six, a deadline that did not move. This is the honest version of how it went.

What stayed on the server

Anything that reads data and does not respond to a finger: tables, detail pages, navigation, the first render of most forms. These got simpler. The data fetching sits next to the markup that uses it and there is no loading state to design, because the page arrives with the data in it.

tsx
// app/invoices/page.tsx: no client JavaScript at all
export default async function Invoices() {
  const rows = await db.invoices.recent(50)
  return (
    <Table>
      {rows.map((r) => <InvoiceRow key={r.id} invoice={r} />)}
    </Table>
  )
}

What moved back

Anything that has to answer in under fifty milliseconds. Filters, drag and drop, the command palette. We tried to keep the filters on the server for a month, with the URL as state. On a good connection it was fine. On a train it was not.

What surprised us

  • The bundle fell by 38%, mostly libraries that only ever formatted dates and money.
  • Caching became the hard problem. We wrote one page of rules and still broke them twice.
  • Tests got easier. A server component is a function that returns markup.

The rule that survived

A component belongs on the client only if the user can change it faster than the network can. Everything else is a document, and documents are what servers are for.

We wrote that sentence on the wall in month three and it has settled every argument since. Would I do the migration again? Yes, but in a quarter, not a sprint, and with the caching rules written first.

Writing

What moved to the server, what moved back, and the one rule that survived.

to read
2 min
published
2026-05-08
filed under
React
A desk with a lamp and a notebook by a rainy city window at night

A year ago I moved a mid-sized product to React Server Components. Forty routes, a team of six, a deadline that did not move. This is the honest version of how it went.

What stayed on the server

Anything that reads data and does not respond to a finger: tables, detail pages, navigation, the first render of most forms. These got simpler. The data fetching sits next to the markup that uses it and there is no loading state to design, because the page arrives with the data in it.

tsx
// app/invoices/page.tsx: no client JavaScript at all
export default async function Invoices() {
  const rows = await db.invoices.recent(50)
  return (
    <Table>
      {rows.map((r) => <InvoiceRow key={r.id} invoice={r} />)}
    </Table>
  )
}

What moved back

Anything that has to answer in under fifty milliseconds. Filters, drag and drop, the command palette. We tried to keep the filters on the server for a month, with the URL as state. On a good connection it was fine. On a train it was not.

What surprised us

  • The bundle fell by 38%, mostly libraries that only ever formatted dates and money.
  • Caching became the hard problem. We wrote one page of rules and still broke them twice.
  • Tests got easier. A server component is a function that returns markup.

The rule that survived

A component belongs on the client only if the user can change it faster than the network can. Everything else is a document, and documents are what servers are for.

We wrote that sentence on the wall in month three and it has settled every argument since. Would I do the migration again? Yes, but in a quarter, not a sprint, and with the caching rules written first.

Start a project

Three quick picks so the first reply is useful. You finish the message on the contact page.

What do you need?

Rough budget

brief.tsdraft, not sent

Brief so far: nothing picked yet

or write to hello@sorenhale.dev

Nothing is sent from this page. Your picks are kept in this browser tab and wait for you on the contact form.

Start a project

Three quick picks so the first reply is useful. You finish the message on the contact page.

What do you need?

Rough budget

brief.tsdraft, not sent

Brief so far: nothing picked yet

or write to hello@sorenhale.dev

Nothing is sent from this page. Your picks are kept in this browser tab and wait for you on the contact form.

Start a project

Three quick picks so the first reply is useful. You finish the message on the contact page.

What do you need?

Rough budget

brief.tsdraft, not sent

Brief so far: nothing picked yet

or write to hello@sorenhale.dev

Nothing is sent from this page. Your picks are kept in this browser tab and wait for you on the contact form.

Footer

Local time, Oslo

Footer

Local time, Oslo

Footer

Local time, Oslo

Create a free website with Framer, the website builder loved by startups, designers and agencies.