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
.