DEV Community

Jack Herrington
Jack Herrington

Posted on • Originally published at jherr2020.Medium on

App Router Secret: Promises for Client Component Props

We know that we can send objects, arrays, strings, numbers, dates and more value types as properties from a React Server Component (RSC) to a client component, but did you know you can send a promise? You can use the new use hook to block on the promise inside the client component!

To see this in action be sure to check out the YouTube video:

In the video we demonstrate not just using the use hook but also using the popular React-Query library because with React-Query (or SWR) you can track a promise.

Let’s dig a little deeper into the React-Query use case by showing you also how to refetch data from the client component on the browser! You can go through the code yourself in this GitHub repository.

Setting up the API

Our simple NextJS App Router application is going to be a timer. When it’s completed it will look like this:

We’ll get that timestamp from an API endpoint on /api/timer . The code for the endpoint is very simple:

import { NextResponse } from "next/server";

export function GET() {
  return NextResponse.json(new Date().toISOString());
}
Enter fullscreen mode Exit fullscreen mode

The home route page component is what makes the request to the API:

export default function Home() {
  const timerPromise = fetch("http://localhost:3000/api/timer", {
    cache: "no-cache",
  }).then((res) => res.json());

  return (
    <main className="max-w-3xl mx-auto mt-5">
      <RQTimer timerPromise={timerPromise} />
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

At the top we make the request to /api/timer but we don’t await it, and that’s important because we can pass that promise to the RQTimer client component.

Using The Use Hook For Promise Data

Let’s start our client component off by just using the use hook to get the data.

"use client";
import { use } from "react";

export default function FoodList({
  timerPromise,
}: {
  timerPromise: Promise<string>;
}) {
  const time = use(timerPromise);
  return (
    <>
      <div className="font-light text-xl mb-2">RQ/RSC Timer</div>
      <div className="font-extrabold text-3xl">{time?.toString()}</div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

The use hook is a new hook that, given a promise, will monitor that promise once it’s fulfilled will force a re-render of the component and then return the time which we format into some decent looking Tailwind.

Didn’t know that you could send a promise as a property? Keep up with these kinds of NextJS tips and tricks by subscribing to the ProNextJS newsletter.

Now that’s all well and good, but let’s say we are React-Query fans, can we use that instead? Sure we can!

Using React Query Instead of Use

To use React-Query we need to wrap the page in a QueryProvider (shown in the repo code). Then we can use useQuery in our RQTimer component like this:

"use client";
import { useQuery } from "@tanstack/react-query";

export default function FoodList({
  timerPromise,
}: {
  timerPromise: Promise<string>;
}) {
  const { data: time } = useQuery(["timer"], () => timerPromise);

  return (
    <>
      <div className="font-light text-xl mb-2">RQ/RSC Timer</div>
      <div className="font-extrabold text-3xl">{time?.toString()}</div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

The useQuery hook requires two parameters. First the query key, which is just a unique key to describe the request, in this case we use timer . And then a function that makes the request, and for that we can just give it a function that returns the timerPromise that we got as a property.

So easy!

But… what if we want to re-fetch that API endpoint once a second? To do that we can use React-Query’s fetchInterval option!

Using FetchInterval To Refetch The API On The Client

So first off we need to only run the refetch of the /api/time API on the client. So we’ll need to use a useEffect to set a flag that we can use to tell if we are on the client.

  const clientLoaded = useRef(false);
  const { data } = useQuery(["timer"], () => timerPromise);

  useEffect(() => {
    clientLoaded.current = true;
  }, []);
Enter fullscreen mode Exit fullscreen mode

So we declare a clientLoaded ref and then set it in the useEffect. Then in our useQuery query function we check that clientLoaded ref.

  const { data } = useQuery(
    ["timer"],
    () => {
      if (clientLoaded.current) {
        return fetch("http://localhost:3000/api/timer", {
          cache: "no-cache",
        }).then((res) => res.json());
      }
      return timerPromise;
    },
    {
      refetchInterval: 1000,
    }
  );
Enter fullscreen mode Exit fullscreen mode

If the clientLoaded ref is true then we know we are on the client and we get the data from the API, otherwise we are on the server (or in the first render on the client) and we return the original timerPromise.

Manually Refetching

In this case we are using the refetchInterval to make a new fetch to the API every second. But if you want you can force the refetch manually by getting the refetch function in the return of the useQuery hook. Then, whenever you want to refetch on the client just call that function.

Top comments (0)