A setup for RSC development
RSC (React Server Components) are very popular now. I have developed a setup to develop SPA with RSC. The setup can be found here (for the version without SSR) and here (for the version with SSR). And you can use it right now and test it in your computer by typing in a terminal window:
npx create-rsc-app@latest my-app
for a setup without SSR and
npx create-rsc-app@latest my-app --ssr
for a setup with SSR. More info can be found at here.
In this post I will present you the setup.
The file-folder structure
When you type npx create-rsc-app@latest rsc-1 you get the following file-folder structure:
There is a Router component in the src/server/components folder. Also we see a Greeting component in this same folder and also a corresponding Greeting component in the src/client/components folder.
So, how this setup works?
Let’s start for the App component in src/client/components folder:
import React, { useState } from "react";
import styled from "styled-components";
import imgReact from "src/client/assets/react.png";
import Image from "./image";
import { RSC } from "rsc-module/client";
import { useAtom } from "src/client/atoms";
export default function App() {
const [softKey, setSoftKey] = useState(0);
const [counter, setCounter] = useAtom("counter");
return (
<Container>
<Title>RSC</Title>
<Image src={imgReact} maxWidth="600px" borderRadius="10px" />
<Div>
<button onClick={() => setSoftKey((sK) => sK + 1)}>
get Greeting of the Day (from server)
</button>
{softKey > 0 && (
<RSC componentName="greeting" softKey={softKey}>
loading greeting ...
</RSC>
)}
</Div>
<Counters>
<div>
<button onClick={() => setCounter((c) => c + 1)}>+</button>
{counter}
</div>
</Counters>
<Div>
This is a setup for RSC (React Server Components) development, without
SSR (Server Side Rendering). It has been bootstrapped with the command{" "}
<strong>npx create-rsc-app@latest my-app</strong>.
</Div>
<Div>
Another setup for RSC development with SSR can be bootsrapped with the
command <strong>npx create-rsc-app@latest my-app --ssr</strong>.
</Div>
<Div>
It has included{" "}
<a href="https://styled-components.com/" target="_blank">
styled-components
</a>{" "}
and{" "}
<a href="https://www.npmjs.com/package/jotai-wrapper" target="_blank">
jotai-wrapper
</a>
, a library around jotai to manage global state.
</Div>
<Div>
With this setup you can build SPA's with secret keys to fetch an API
hidden from the Client (browser) or fetch some database in the server
with Prisma.
</Div>
</Container>
);
}
const Div = styled.div`
text-align: center;
`;
const Title = styled(Div)`
font-size: 2rem;
font-weight: 700;
`;
const Container = styled.div`
font-family: sans-serif;
height: 97vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
`;
const Counters = styled.div`
display: flex;
gap: 10px;
`;
Two things to note first from the above code: we use styled-components and jotai (jotai-wrapper) for state management.
Second part important of the above code is this:
import { RSC } from "rsc-module/client";
// ...
<RSC componentName="greeting" softKey={softKey}>
loading greeting ...
</RSC>
// ...
This is the core idea of this setup. There is a module instaled, rsc-module, which gives us the capabilities of RSC. By importing the client component ‘RSC’ we can use it to fetch the server for a RSC. In this case we are fetching the Greeting RSC. And this Greeting RSC will return the Greeting RCC (React Client Component) with fetched data passed as props to it:
import React from "react";
import { RCC } from "rsc-module/server";
export default async function Greeting() {
const value = Math.random() < 0.5;
const greeting = await new Promise((r) =>
setTimeout(() => {
switch (value) {
case true:
return r("Whatsupp!!!");
case false:
return r("How r u doing?");
}
}, 500)
);
return <RCC __isClient__="components/greeting" greeting={greeting} />;
}
The way to return a client component in this setup is with this:
return <RCC __isClient__="components/greeting" greeting={greeting} />;
‘RCC’ is a special RSC that does nothing. The important part is the prop __isClient__ we pass to it. In it we state where to find the client component we want to return respect the src/client folder. In this case we want to return the Greeting RCC located in src/client/components/greeting, so __isClient__ will be “components/greeting”. We also pass the rest of the props for this component to be used, in this case greeting prop.
Implementation details: the rsc-module.
In the rsc-module there are the implementation details. In this module we find the ‘RSC’ RCC and the ‘RCC’ RSC we have told before. This is the implementation detail of the ‘RSC’ RCC:
import React, { useMemo, Suspense } from "react";
import { fillJSXWithClientComponents, parseJSX } from "../utils/index.js";
import { usePropsChangedKey } from "../hooks/use-props-changed-key.js";
import useSWR from "swr";
const Error = ({ errorMessage }) => <>Something went wrong: {errorMessage}</>;
const fetcher = (componentName, body) =>
fetch(`/${componentName}`, {
method: "post",
headers: { "content-type": "application/json" },
body,
})
.then((response) => response.text())
.then((clientJSXString) => {
const clientJSX = JSON.parse(clientJSXString, parseJSX);
return fillJSXWithClientComponents(clientJSX);
})
.catch((error) => {
return <Error errorMessage={error.message} />;
});
const fetcherSWR = ([, componentName, body]) => fetcher(componentName, body);
const getReader = () => {
let done = false;
let promise = null;
let value;
return {
read: (fetcher) => {
if (done) {
return value;
}
if (promise) {
throw promise;
}
promise = new Promise(async (resolve) => {
try {
value = await fetcher();
} catch (e) {
value = errorJSX;
} finally {
done = true;
promise = null;
resolve();
}
});
throw promise;
},
};
};
const Read = ({ fetcher, reader }) => {
return reader.read(fetcher);
};
const ReadSWR = ({ swrArgs, fetcher }) => {
return useSWR(swrArgs, fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
suspense: true,
}).data;
};
export function RSC({
componentName = "__no_component_name__",
children = <>loading ...</>,
softKey,
isSWR = false,
...props
}) {
const propsChangedKey = usePropsChangedKey(...Object.values(props));
const body = useMemo(() => {
return JSON.stringify({ props });
}, [propsChangedKey]);
const reader = useMemo(() => getReader(), [propsChangedKey, softKey]);
const swrArgs = useMemo(
() => [softKey, componentName, body],
[componentName, body, softKey]
);
return (
<Suspense fallback={children}>
{isSWR ? (
<ReadSWR swrArgs={swrArgs} fetcher={fetcherSWR} />
) : (
<Read fetcher={() => fetcher(componentName, body)} reader={reader} />
)}
</Suspense>
);
}
And this is the implementation detail of the ‘RCC’ RSC found in this module:
export async function RCC() {
return null;
}
The Router RSC.
The Router RSC is a special RSC of the implementation. We don’t call it directly from the client. Instead all requests made to the server from the client passes through it. It is encharged to fetch the right RSC we are looking for. This is its implementation:
import React from "react";
import { RCC } from "rsc-module/server";
import Greeting from "./greeting.js";
export default async function Router({ componentName, props }) {
switch (componentName) {
case "greeting":
return <Greeting {...props} />;
default:
return <RCC __isClient__="components/ups" />;
}
}
It receives componentName prop and props prop. It uses componentName prop to fetch the right RSC. When found, it returns it passing to it the props prop destructured. If not found any RSC that matches componentName prop then it returns a default RCC. Keep in mind that in this implementation all RSC’s return RCC’s.
A note about ‘RSC’ RCC
The ‘RSC’ RCC is like a barrier for functions to be passed as props in the React tree, because functions cannot be stringified. In that case what you should do is store the value of the function in the global state and recover it downwards in the tree.
The origin of this setup
This setup is the response to last challenge exposed by Dan Abramov in his post about RSC + SSR implementation from scratch. He challenged people to add RCC to his implementation.
From here, what to do?
With this setup you can do fullstack apps with React. You can either consume an API with secret keys hidden from the Client or you can use Prisma to fetch a database. It’s up to you.
Summary
We have shown how to develop with this setup for RSC (React Server Components). In this setup you call your RSC’s with the ‘RSC’ RCC (React Client Component). The RSC’s return all RCC’s with the fetched data passed as props.