slhck
slhck

Reputation: 38652

Pushing changes from server to client components in Next.js 13 app dir

I'm playing around with Next.js 13's app dir, and I want to replicate some functionality I have with the "classic" pages dir. I want to display a grid of cards, one each for a set of hosts. I want to ping the hosts in the background, and each card should show whether the host is reachable or not.

So a card looks like this:

app/clients/client-card.tsx:

"use client";

import { Card, CardBody } from "../components";
import { Chip } from "@material-tailwind/react";

export type ClientProps = {
  label: string;
  available?: boolean | undefined;
};

export default function ClientCard({ label, available }: ClientProps) {
  return (
    <Card key={label} title={label}>
      <CardBody>
        <h4>{label}</h4>
        <Chip
          color={available === undefined ? "gray" : available ? "green" : "red"}
          className="mt-2"
          value={available === undefined ? "Checking" : available ? "Online" : "Offline"}
        />
      </CardBody>
    </Card>
  );
}

And the server code in app/clients/page.tsx:

import fs from "fs";
import path from "path";
import { load } from "js-yaml";
import ClientCard from "./client-card";

type AnsibleInventory = {
  probes?: {
    hosts: Record<string, unknown>;
  };
};

async function getInventory() {
  const inventoryFile = path.join(
    __dirname,
    "..",
    "..",
    "..",
    "..",
    "..",
    "example",
    "inventory",
    "hosts.yml"
  );
  const inventory = load(
    fs.readFileSync(inventoryFile, "utf8")
  ) as AnsibleInventory;

  return inventory;
}

export default async function Clients() {
  const inventory = await getInventory();

  const isAvailable = async (client: string): Promise<boolean> => {
    const sleep = (ms: number) =>
      new Promise((resolve) => setTimeout(resolve, ms));
    await sleep(1000 + Math.random() * 1000);
    return Math.random() > 0.5;
  };

  return (
    <div>
      <h1>Clients</h1>
      {inventory.probes?.hosts === undefined ? (
        <p>No clients found</p>
      ) : (
        <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {Object.keys(inventory.probes?.hosts).map((client) => (
            <div key={client} className="col-span-1">
              <ClientCard
                key={client}
                label={client}
                available={isAvailable(client)}
              />
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

The problem is, I want the page to load initially first, with each card showing a "Checking" state and gray chip, and propagate the results of the check. to each of the cards later on. But I can't asynchronously call the isAvailable() function—it needs a synchronous value. And I can't use useState and effects on the server side.

Previously I would have probably created a dedicated api route and made a request from each client-side card to the server to fetch the availability status from the API.

Is there any simple way to do this? In particular, this is a potentially long list of cards for individual hosts …

I see that in one of the streaming examples, a fetch is passed to the individual components, but those appear to be server-side, too. In my case, I apparently must use "use client"; due to some client-side effects being used by the @material-tailwind/react library, so I can't make it a server component.

Upvotes: 1

Views: 389

Answers (1)

slhck
slhck

Reputation: 38652

There seems to be no obvious solution for now, other than to separate the libraries such that I don't import a client side library in the RSC.

Upvotes: 0

Related Questions