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.