If we want to use React 19, one of the best options is to use Waku. It's a minimal React 19 framework made by the author of Jotai.
React 19 leverages the power of Server Components and Server Actions. So this post it's about how to fetch data from the server through Server Actions once we are on the Client side.
Let's say we have a page like this in Waku:
//src/pages/index.tsx
import HomePageClient from "../components/home-page-client";
export default async function HomePageServer() {
return <HomePageClient />;
}
As you can see HomePageServer
is a React Server Component. We are calling HomePageClient
, which will be a React Client Component:
//src/components/home-page-client.tsx
"use client";
import { sayHello } from "../server-actions/say-hello";
import { Suspense, useEffect, useState } from "react";
export default function HomePageClient() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
return isClient ? (
<Suspense fallback="Loading...">{sayHello()}</Suspense>
) : null;
}
In this component we are calling directly a Server Action wrapped in a Suspense
component. This server action will return a component (Client or not). Like this:
//src/server-actions/say-hello.tsx
"use server";
import SayHello from "../components/say-hello";
export function sayHello() {
const promise = new Promise<string[]>((r) =>
setTimeout(() => r(["Roger", "Alex"]), 1000)
);
return <SayHello promise={promise} />;
}
You see how it returns SayHello
component. Another important part of this Server Action is that it doesn't await
for the promise to fulfill but it passes the promise as is to the component returned.
So this is the SayHello
component returned by the Server Action:
//src/components/say-hello.tsx
"use client";
import { Suspense, use } from "react";
export default function SayHello({ promise }: { promise: Promise<string[]> }) {
const Comp = () => {
const data = use(promise);
return data.map((item) => <div key={item}>{item}</div>);
};
return (
<>
<div>hey</div>
<div>
<Suspense fallback="loading###">
<Comp />
</Suspense>
</div>
</>
);
}
Because we needed to access to the resolved value of the promise (an array of strings), we defined a component (Comp
) specially for this purpose, that uses use
from React, and wrapped it in a Suspense
component. In this way the hey
content can be displayed immediately, without waiting for the promise to resolve.
There is room for optimization to this approach, and that is to use EnhancedSuspense
from react-enhanced-suspense:
//src/components/say-hello.tsx
"use client";
import { EnhancedSuspense } from "react-enhanced-suspense";
export default function SayHello({ promise }: { promise: Promise<string[]> }) {
return (
<>
<div>hey</div>
<div>
<EnhancedSuspense
fallback="Loading###"
onSuccess={(data) => data.map((item) => <div key={item}>{item}</div>)}
>
{promise}
</EnhancedSuspense>
</div>
</>
);
}
The code for the EnhancedSuspense
component is (if you are curious):
import { JSX, ReactNode, Suspense, use } from "react";
import ErrorBoundary from "./error-boundary.js";
type EnhancedSuspenseProps<T> = {
fallback?: ReactNode;
children?: Promise<T> | JSX.Element | undefined | string;
onSuccess?: ((data: T) => ReactNode) | undefined;
onError?: (error: Error) => ReactNode;
};
const EnhancedSuspense = <T,>({
fallback = "Loading...",
children: promise,
onSuccess,
onError,
}: EnhancedSuspenseProps<T>) => {
const Use = () => {
if (!promise) return null;
if (
typeof promise === "string" ||
("props" in promise && "type" in promise)
) {
return promise;
}
const data = use(promise);
return onSuccess ? onSuccess(data) : (data as ReactNode);
};
return (
<ErrorBoundary onError={onError}>
<Suspense fallback={fallback}>
<Use />
</Suspense>
</ErrorBoundary>
);
};
export default EnhancedSuspense;
Important note regarding deployment/build
The above (return client components by server actions called on client components) works in local or during development phase in Waku. But when you try to build/deploy the project it fails with the following error:
[rsc-transform-plugin] client id not found: //...
This is because the client component returned by the server action has not been used never in the JSX tree. A workaround to this problem is to use the client component returned by the server action in a server component like this:
import "../styles.css";
import type { ReactNode } from "react";
import SayHello from "../components/say-hello"; // 1. import the client component returned by server action
type RootLayoutProps = { children: ReactNode };
export default async function RootLayout({ children }: RootLayoutProps) {
const data = await getData();
return (
<div className="font-['Nunito']">
<meta name="description" content={data.description} />
<link rel="icon" type="image/png" href={data.icon} />
<main className="m-6 flex items-center *:min-h-64 *:min-w-64 lg:m-0 lg:min-h-svh lg:justify-center">
{children}
</main>
{/* 2. use like this the component in the layout to fix deploy/build error */}
{false && <SayHello />}
</div>
);
}
This doesn’t affect functionality of the app and fix the problem on build/deploy. This must be done for every client component returned by server action. In this case we have done it in the _layout.tsx
file but can be done also in the react server component page or route file.
This is a Repo to this project.
Summary
In this post we have seen how the leverage of React Server Components and Server Actions by React 19, combined with the use of Suspense
component and use
function from React, allows us to fetch data once on the client side in an efficient manner, rendering parts of a component while others are still waiting for a promise to fulfill.
We have seen how the EnhancedSuspense
from react-enhanced-suspense helps us in dealing with promises in React 19 components. See the link for full documentation and use cases about this component.
We have also seen how Waku it's a great way to start using React 19 right now.
Thanks.
Top comments (0)