This example is a server driven app built with Parcel and React Server Components. In this setup, routing happens on the server, delivering HTML on initial page load, and client side rendering on subsequent navigations. It also demonstrates React Server Actions to perform mutations, both by calling as a function and as the target of an HTML form.
The example consists of the following main files:
This is the main server entrypoint, built using Express. It is the entry of the Parcel build. All other client and server dependencies are discovered from here.
This is the entry React Server Component that renders the root <html>
element, server content, and any client components. It is marked with the Parcel-specific "use server-entry"
directive, which creates a code splitting entrypoint. Common dependencies between pages are extracted into shared bundles.
Server component entries must render the <Resources>
component from @parcel/runtime-rsc
. This is an automatically generated React component that renders the necessary <script>
and <link rel="stylesheet">
elements needed to render the page. This includes injecting the script containing the necessary client components for the page, as well as any CSS imported by both client and server components.
This is the main client entrypoint, imported from each page. It uses the Parcel-specific "use client-entry"
directive to mark that it should only run on the client, and not on the server (even during SSR). The client is responsible for hydrating the initial page, and intercepting link clicks and navigations to perform client side routing.
This is a server actions file. Functions exported by this file can be imported from the client and called to send data to the server for processing. It is marked using the "use server"
directive. When Parcel sees this directive, it places the actions into the server bundle, and creates a proxy module on the client that calls the server action via the handler in client.tsx
.
Currently, server actions must be defined in a separate file. Inline server actions (e.g. "use server"
inside a function) are not supported by Parcel.
These are client components. <TodoItem>
renders a todo list item, and uses server actions and useOptimistic
to implement the checkbox and remove buttons. Dialog.tsx
renders a dialog component using client APIs, and accepts the create todo form (which is a server component) as children.
The flow of initial rendering starts on the server.
The server handles routing using Express. When a route handler is called, it performs the following steps:
- Render the relevant page component to an RSC payload using
renderToReadableStream
fromreact-server-dom-parcel/server
. - If the
Accept
header includestext/html
, create an RSC client usingcreateFromReadableStream
fromreact-server-dom-parcel/client
. This must be imported with the{env: 'react-client'}
import attribute so that it runs in a client environment. - Use a client copy of React (imported with
{env: 'react-client'}
) to render the RSC payload to HTML. This involves creating a component that calls the client React'suse
hook to consume the RSC payload, and rendering it to HTML with the usualrenderToReadableStream
API fromreact-dom/server
. - Embed the RSC payload into the HTML stream using
injectRSCPayload
fromrsc-html-stream/server
. This will be used by the client during hydration.
To hydrate the initial page, the client performs the following steps.
- Read the embedded RSC payload from the initial HTML using
createFromReadableStream
fromreact-server-dom-parcel/client
, and the RSC stream fromrsc-html-stream/client
. - Create a root
Content
component. This stores the current root element in React state. Initially this is set touse(initialRSCPayload)
, but can be updated when client navigations occur.
The client also includes a very simple router, allowing subsequent navigations after the initial page load to maintain client state without reloading the full HTML page.
The client listens for the click
event for all link elements on the page using event delegation, as well as the popstate
event to detect when the user navigates with the browser back button. To perform a navigation:
- Create a fetch request for the route, with the
Accept
header set totext/x-component
to request an RSC payload. - Call
createFromFetch
fromreact-server-dom-parcel/client
to create an RSC stream. - In a React Transition (via
startTransition
), call the storedupdateRoot
function created during the initial render to update the root element in theContent
component. - Once the new page is finished loading, push the new URL to the browser's history with
history.pushState
.
These steps can be customized as needed for your server setup, e.g. using a better client side router, or adding authentication headers.
The server handles fetch requests for RSC payloads using the same route handlers as for HTML. When a route handler is called, it performs the following steps:
- Render the page component to an RSC payload using
renderToReadableStream
fromreact-server-dom-parcel/server
. - Respond with the RSC payload directly if the
Accept
header does not includetext/html
.
Server actions allow the client to call the server to perform mutations and other actions. There are two ways server actions can be called: by calling an action function from the client, or by submitting an HTML form.
When a server action is called, the client is responsible for sending a request to the server. This is done by registering a handler with setServerCallback
from react-server-dom-parcel/client
. When a server action proxy function generated by Parcel is called on the client, this handler will be invoked with the id of the action, and the arguments to pass to it.
- Create a
POST
request usingfetch
, and set thersc-action-id
header to the id of the action to call. - Use the
encodeReply
function fromreact-server-dom-parcel/client
to set the body of the request. React will encode the arguments to the action using the RSC protocol. - Use
createFromFetch
fromreact-server-dom-parcel/client
to create an RSC stream from the request. The server will return a new component to render, along with the return value of the server action. - In a React Transition (via
startTransition
), callupdateRoot
to update the root element in theContent
component with the component returned by the server. - Return the result of the server action. This will be returned by the proxy function originally called in the client component.
These steps can be customized as needed for your server setup, e.g. adding authentication headers.
When the POST
request handler is called, the server performs the following steps:
- If the client sent a
rsc-action-id
header, the action was sent by the RSC client usingfetch
: - Load the server action function using
loadServerAction
fromreact-server-dom-parcel/server
. Each server action has a corresponding id, which is generated by Parcel during the build. - Decode the arguments to call the action with using
decodeReply
fromreact-server-dom-parcel/server
. This can accept eitherFormData
(when sending things like files), or RSC's text-based encoding. - Call the server action function with the loaded arguments, and await the result.
- Respond to the HTTP request by rendering the server component, following the steps above, and passing back the promise returned by the action as the result. This will be returned as the result of the action on the client.
- Otherwise, the action may have been called directly by submitting an HTML
<form>
element via progressive enhancement. - Call the
decodeAction
function to load the action function. This will automatically be bound with theFormData
as an argument to the action. - Call the server action function, and await the result.
- Respond to the HTTP request by rendering the server component, following the steps above.