React 19, Waku, and the use of Server Actions to fetch data once on the Client side

If we want to use React 19, one of the best options is to use Waku. It's a lightweight 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 ; } 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, useState } from "react"; export default function HomePageClient() { const [start, setStart] = useState(false); return ( setStart(true)}>start {start ? {sayHello()} : 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((r) => setTimeout(() => r(["Roger", "Alex"]), 1000) ); return ; } 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 }) { const Comp = () => { const data = use(promise); return data.map((item) => {item}); }; return ( hey ); } 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 a little optimization to this approach, and that is to create a component like this: //src/components/suspense-with-use.tsx import { ReactNode, Suspense, use } from "react"; type UseProps = { promise: Promise; children: (data: T) => ReactNode; }; const Use = ({ promise, children }: UseProps) => { const data = use(promise); return children(data); }; type SuspenseWithUseProps = { fallback: ReactNode; } & UseProps; const SuspenseWithUse = ({ fallback, promise, children, }: SuspenseWithUseProps) => ( {children} ); export default SuspenseWithUse; Now we can use this component to render the content resolved by the promise in a more straightforward way: //src/components/say-hello.tsx "use client"; import SuspenseWithUse from "./suspense-with-use"; export default function SayHello({ promise }: { promise: Promise }) { return ( hey {(data) => data.map((item) => {item})} ); } 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 also seen how Waku it's a great way to start using React 19 right now. Thanks.

Mar 14, 2025 - 07:19
 0
React 19, Waku, and the use of Server Actions to fetch data once on the Client side

If we want to use React 19, one of the best options is to use Waku. It's a lightweight 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, useState } from "react";

export default function HomePageClient() {
  const [start, setStart] = useState(false);

  return (
    <>
      <button onClick={() => setStart(true)}>start</button>
      {start ? <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 a little optimization to this approach, and that is to create a component like this:

//src/components/suspense-with-use.tsx
import { ReactNode, Suspense, use } from "react";

type UseProps<T> = {
  promise: Promise<T>;
  children: (data: T) => ReactNode;
};

const Use = <T,>({ promise, children }: UseProps<T>) => {
  const data = use(promise);
  return children(data);
};

type SuspenseWithUseProps<T> = {
  fallback: ReactNode;
} & UseProps<T>;

const SuspenseWithUse = <T,>({
  fallback,
  promise,
  children,
}: SuspenseWithUseProps<T>) => (
  <Suspense fallback={fallback}>
    <Use promise={promise}>{children}</Use>
  </Suspense>
);

export default SuspenseWithUse;

Now we can use this component to render the content resolved by the promise in a more straightforward way:

//src/components/say-hello.tsx
"use client";

import SuspenseWithUse from "./suspense-with-use";

export default function SayHello({ promise }: { promise: Promise<string[]> }) {
  return (
    <>
      <div>hey</div>
      <div>
        <SuspenseWithUse fallback="Loading###" promise={promise}>
          {(data) => data.map((item) => <div key={item}>{item}</div>)}
        </SuspenseWithUse>
      </div>
    </>
  );
}

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 also seen how Waku it's a great way to start using React 19 right now.

Thanks.