RSC

The App router from Next.js introduced a lot of weird rules, for example, around partial page updates and access to the URL on the server. But they have nothing to do with RSC (React Server Components) and everything to do with Next.js. There are actually very few rules when it comes to RSC, as you can see with the Navigation router. All the Navigation router documentation, apart from the setup, works the same whether you're using RSC or not.

Setting up the Bundler

The Navigation router has Parcel and Webpack RSC samples you can compare. Although their setup code is very different, the application code is identical. The following covers the setup files from the Parcel example since Webpack's RSC support isn't production-ready. The 3 items marked with * are the ones you edit with your scenes.

To RSC-ify our email app example, we copy these files over and make the changes outlined above. In the server.ts file, for example, we add our 3 scenes to the sceneViews lookup in the post handler. We also copy over the typescript setup and all the dependencies from the package.json of the Parcel sample and run npm install.

app.post('*', async (req, res) => {
  const sceneViews: any = {
    inbox: Inbox,
    mail: Mail,
    compose: Compose
  };
  const {url, sceneViewKey} = req.body;
  const View = sceneViews[sceneViewKey];

With RSC, we fetch data on the server instead of in useEffects on the client. The shift to the server enables SSR and avoids waterfalls. We turn on data-fetching in our Inbox and Mail components by marking them as async. We also add the use server-entry directive to our 3 scenes to tell Parcel where to bundle-split.

'use server-entry'

const Mail = async () => {
  const {data} = useNavigationEvent();
  const {id, expand} = data;
  const mail = await fetchMail(id);
};

Refetching Scenes

Our email app loads fast because we've removed the data-fetching waterfalls. But we've accidentally made it slower when the user expands an email. We used to handle this client-side but now we're expanding the email on the server. We move this back to the client by extracting the expansion code into a client component.

'use client'

const Thread = ({mail}) => {
  const {data} = useNavigationEvent();
  const {expand} = data;
  if (!expand) return null;
  return (
    <ul>
      {mail.thread.map({id, content}) => (
        <li key={id}>{content}</li>
      )}
    </ul>
  )
};

Although the expansion happens on the client, it still has to wait for a server round-trip. By default, the SceneView refetches on every navigation. But it has a refetch prop that tells it to only refetch when specific navigation data changes. We pass 'id' so the 'mail' scene only refetches when the email changes, removing the unnecessary round-trip when the email expands.

<SceneView active="mail" refetch={['id']}>
  <Mail />
</SceneView>

Handling Mutations

The Navigation router doesn't support server actions. Instead, create a client component and call your api mutation endpoints as you would in a traditional SPA. The useRefetch hook returns a function you can use to update the whole scene after the mutation completes. In our example app, we refetch the 'mail' scene when the user sends a reply so that the new reply is appended to the email thread.

import {useRefetch} from 'navigation-react';

const refetch = useRefetch();

onClick={() => {
  // send reply
  refetch();
}}