DEV Community

Cover image for React 19, Waku, react-enhanced-suspense and Server Actions: how to fetch data efficiently in the Client
roggc
roggc

Posted on • Edited on

React 19, Waku, react-enhanced-suspense and Server Actions: how to fetch data efficiently in the Client

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 />;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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} />;
}
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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: //...
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

Image of Datadog

Create and maintain end-to-end frontend tests

Learn best practices on creating frontend tests, testing on-premise apps, integrating tests into your CI/CD pipeline, and using Datadog’s testing tunnel.

Download The Guide

Top comments (0)

Hot sauce if you're wrong - web dev trivia for staff engineers

Hot sauce if you're wrong · web dev trivia for staff engineers (Chris vs Jeremy, Leet Heat S1.E4)

  • Shipping Fast: Test your knowledge of deployment strategies and techniques
  • Authentication: Prove you know your OAuth from your JWT
  • CSS: Demonstrate your styling expertise under pressure
  • Acronyms: Decode the alphabet soup of web development
  • Accessibility: Show your commitment to building for everyone

Contestants must answer rapid-fire questions across the full stack of modern web development. Get it right, earn points. Get it wrong? The spice level goes up!

Watch Video 🌶️🔥

AWS GenAI LIVE!

GenAI LIVE! is a dynamic live-streamed show exploring how AWS and our partners are helping organizations unlock real value with generative AI.

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️