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, Webpack and Vite 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 and Vite RSC support aren'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
  };

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>

Server Functions

In our email example app, we call a sendReply Server Function when the user replies to an email. By default, the Navigation router doesn't refetch the scene after a Server Function call. We can trigger the refetch by wrapping our Server Function in useSceneView. It then receives an extra refetch parameter which we call after a successful send. Then the scene updates and the new reply is added to the email thread.

'use server'
export async function sendReply(mail, {refetch}) {
  // send reply
  refetch();
}

'use client'
import {useSceneView} from 'navigation-react';

const send = useSceneView(sendReply);
<button onClick={() => send(mail)} />

This extra parameter the Server Function receives also holds a StateNavigator so you can navigate to a new scene instead of refetching the current one. For example, after the user composes a new email, we navigate them back to their inbox.

export async function compose(mail, {stateNavigator}) {
  // send mail
  stateNavigator.navigate('inbox');
}