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.
- App.tsx* - server component shell where you render your scenes
-
stateNavigator.ts* - returns a
StateNavigator
you configure with your scenes -
server.tsx* - web server that streams RSC. You list your scenes inside the
post
handler - client.tsx - hydrates the App from the initial RSC stream
-
NavigationProvider.tsx - renders the
NavigationHandler
- HmrProvider.tsx - supports hot module reoloading
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();
}}