So far, we only have one page. To add another, create a new route in the server code, along with a new component to render.
import {About} from './About';
// ...
app.get('/about', async (req, res) => {
await renderRequest(req, res, <About />, {component: About});
});
"use server-entry";
import './client';
export function Page() {
return (
<html>
<head>
<title>About</title>
</head>
<body>
<h1>About</h1>
<a href="/">Home</a>
</body>
</html>
);
}
Now you should be able to load http://localhost:3000/about.
However, you may notice that when clicking the "Home" link, the browser does a full page refresh. To improve the responsiveness of navigations, you can fetch a new RSC payload from the server and update the component tree in place instead.
@parcel/rsc/client
includes a fetchRSC
function, which is a small wrapper around the fetch
API that returns a new React tree. Passing this to the updateRoot
function returned by hydrate
will update the page with the new content.
As a simple example, we can intercept the click
event on links to trigger navigations. The browser history.pushState
API can be used to update the browser's URL bar once the page is finished loading.
"use client-entry";
import {hydrate, fetchRSC} from '@parcel/rsc/client';
let updateRoot = hydrate();
async function navigate(pathname, push = false) {
let root = await fetchRSC(pathname);
updateRoot(root, () => {
if (push) {
history.pushState(null, '', pathname);
}
});
}
// Intercept link clicks to perform RSC navigation.
document.addEventListener('click', e => {
let link = e.target.closest('a');
if (link) {
e.preventDefault();
navigate(link.pathname, true);
}
});
// When the user clicks the back button, navigate with RSC.
window.addEventListener('popstate', e => {
navigate(location.pathname);
});
React Server Functions allow Client Components to call functions on the server, for example, updating a database or calling a backend service.
Server functions are marked with the standard React "use server"
directive. Currently, Parcel supports "use server"
at the top of a file, and not inline within a function.
Server functions can be imported from Client Components and called like normal functions, or passed to the action
prop of a <form>
element.
"use server";
export function createAccount(formData) {
let username = formData.get('username');
let password = formData.get('password');
// ...
}
import {createAccount} from './actions';
export function CreateAccountForm() {
return (
<form action={createAccount}>
<input name="username" />
<input type="password" name="password" />
</form>
)
}
The last step is "connecting" the client and server by making an HTTP request when an action is called. The hydrate
function in @parcel/rsc/client
accepts a handleServerAction
function as an option. When a server action is called on the client, it will go through handleServerAction
, which is responsible for making a request to the server.
"use client-entry";
import {hydrate, fetchRSC} from '@parcel/rsc/client';
let updateRoot = hydrate({
// Setup a callback to perform server actions.
// This sends a POST request to the server and updates the page.
async handleServerAction(id, args) {
let {result, root} = await fetchRSC('/', {
method: 'POST',
headers: {
'rsc-action-id': id,
},
body: args,
});
updateRoot(root);
return result;
},
});
// ...
On the server, we'll need to handle POST requests and call the original server function. This will read the id of the server action passed as an HTTP header, and call the associated action. Then it will re-render the requested page with any updated data, and return the function's result.
import {renderRequest, callAction} from '@parcel/rsc/node';
// ...
app.post('/', async (req, res) => {
let id = req.get('rsc-action-id');
let {result} = await callAction(req, id);
let root = <Page />;
if (id) {
root = {result, root};
}
await renderRequest(req, res, root, {component: Page});
});
This setup can also be customized to change how you call the server, for example, adding authentication headers, or even using a different transport mechanism entirely. Once the setup is complete, you can add additional server actions by exporting async functions from a file with "use server"
, and they will all go through handleServerAction
.
@parcel/config-react-static
enables Parcel to pre-render React Server Components to static HTML at build time.
To set up a new project with static rendering, run the following commands:
npm create parcel react-static my-static-site
cd my-static-site
npm start
Replace npm
with yarn
or pnpm
to use your preferred package manager. See below for a deep dive.
{
"extends": "@parcel/config-react-static"
}
{
"source": "pages/**/*.tsx"
}
With this configuration, components in the pages
directory will be rendered to HTML files in the dist
directory. Entry components receive a list of pages as a prop, which allows you to render a navigation list.
import type {PageProps} from '@parcel/rsc';
export default function Index({pages, currentPage}: PageProps) {
return (
<html>
<body>
<nav>
<ul>
{pages.map(page => (
<li key={page.url}>
<a
href={page.url}
aria-current={page.url === currentPage.url ? 'page' : undefined}>
{page.name.replace('.html', '')}
</a>
</li>
))}
</ul>
</nav>
</body>
</html>
);
}
MDX is a variant of Markdown that compiles to JSX. Parcel supports MDX out of the box, and when used with @parcel/config-react-static
, it will be rendered to static HTML at build time.
{
"source": "pages/**/*.mdx"
}
import Layout from '../src/MDXLayout';
export default Layout;
# Hello, MDX!
This is a static MDX file.
import type {PageProps} from '@parcel/rsc';
import './client';
interface LayoutProps extends PageProps {
children: ReactNode
}
export default function Layout({children, pages, currentPage}: LayoutProps) {
return (
<html lang="en">
<head>
<title>{currentPage.meta.tableOfContents?.[0].title}</title>
</head>
<body>{children}</body>
</html>
);
}
"use client-entry";
import {hydrate} from '@parcel/rsc/client';
hydrate();