Next.js Discord

Discord Forum

How would I pass dynamic {children} to a client component from a server component

Unanswered
Knopper gall posted this in #help-forum
Open in Discord
Knopper gallOP
import { ToggleableButton } from "@/components/toggleable-button";
import BotManager from "@/lib/botManager";

const Bot = new BotManager();

async function callback() {
  "use server";
  return Bot.isRunning ? await Bot.stop() : await Bot.start();
}

export default function Home() {
  return (
    <main>
      <ToggleableButton callback={callback} initialState={Bot.isRunning}>
        {Bot.isRunning ? "Stop" : "Start"}
      </ToggleableButton>
    </main>
  );
}


ToggleableButton is a client component, Bot.isRunning is changing but not updating the text, how woluld I do this without making the whole component it's own separate component in another file?

30 Replies

Knopper gallOP
I figured I can revalidateTag but that seems overkill?
Knopper gallOP
This is how I've cheesed it

import { ToggleableButton } from "@/components/toggleable-button";
import BotManager from "@/lib/botManager";
import { unstable_cache as cache, revalidateTag } from "next/cache";

const Bot = new BotManager();

async function callback() {
  "use server";
  const status = Bot.isRunning ? await Bot.stop() : await Bot.start();
  revalidateTag('btnText');
  return status;
}

export default function Home() {
  return (
    <main>
      <ToggleableButton callback={callback} initialState={Bot.isRunning}>
        { cache(async () => Bot.isRunning ? "Stop" : "Start", [], { tags: ['btnText'] })() }
      </ToggleableButton>
    </main>
  );
}
@Knopper gall solved?
@Knopper gall It's the updating after actioning the callback that I'm having the issue with
can you clarify what specifically does not work? This might help you: https://stackoverflow.com/help/how-to-ask
@B33fb0n3 can you clarify what specifically does not work? This might help you: https://stackoverflow.com/help/how-to-ask
Knopper gallOP
I've already explained, I appreciate your help but there's no need for the passive aggressiveness 🙂

So my component is a client component, it updates colour based on useTransition of the server action callback

The issue I'm having is how to update the button text {children} passed from the server component to the child, without revalidating the cache
If you need more information I'm happy to answer specifics
that I'm having the issue with

this is not very helpful for me... I don't know what to ask, to help you. That's why I asked, that you clarify where you specifically have issues so I can help you more.

I understood your initial problem and I will help you with that if I know where you are stuck
I am passing the state it should be in the server component. Via {children} inside the client component tags
So it is updating but the cache is stale, revalidating the cache makes it update the DOM
but that's not graceful, is it
@Knopper gall so I'm back with my intitial code, how would I gracefully update the button text without hardcoding it into the client component?
yes, then you can manage the state in your client component. So you click the button (update the state instantly. Thought the update the button text will be updated), exectue the callback (server action).

await the result of your server action, to update your optimistic clientside state. Like that you can instantly update the button text and also validate that the boot up (in this case) was successful
@Knopper gall I can still pass the button text in {children} though?
yes you can and conditionally render the content
@B33fb0n3 yes you can and conditionally render the content
Knopper gallOP
I feel like I'm having a moment of being stupid ngl

"use client";

import { Button, ButtonProps } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { useTransition, useState } from 'react';

export function ToggleableButton(
    {
        variants: buttonVariants = { on: "success", off: "destructive" },
        icons: buttonIcons = { on: "play_arrow", off: "stop" },
        initialState = false,
        children: buttonText,
        callback,
    }: { 
        variants?: { on: ButtonProps["variant"], off: ButtonProps["variant"] },
        icons?: { on: string, off: string },
        initialState?: boolean,
        children?: React.ReactNode,
        callback?: () => any,
    }) {

    const [isButtonOn, setIsButtonOn] = useState(initialState);
    const [isPending, startTransition] = useTransition();

    const spanClassList = ["icon"];
    if (buttonText) spanClassList.push("mr-1");
    if (isPending) spanClassList.push("animate-spin");

    const iconJSX = (
        <span className={cn(spanClassList)}>
            {isPending ? "autorenew" : isButtonOn ? buttonIcons.off : buttonIcons.on}
        </span>
    )

    return (
        <Button
            variant={isButtonOn ? buttonVariants.off : buttonVariants.on}
            onClick={() => startTransition(async () => { setIsButtonOn(callback && await callback()) })}
            disabled={isPending}
        >
            {(isButtonOn ? buttonIcons.off : buttonIcons.on) && iconJSX}
            {buttonText}
        </Button>
    )
}


This is my client component, so how would I make the text dynamic, but passed via children instead of as a prop?
I get I need to store the button text as state
@Knopper gall I feel like I'm having a moment of being stupid ngl ts "use client"; import { Button, ButtonProps } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { useTransition, useState } from 'react'; export function ToggleableButton( { variants: buttonVariants = { on: "success", off: "destructive" }, icons: buttonIcons = { on: "play_arrow", off: "stop" }, initialState = false, children: buttonText, callback, }: { variants?: { on: ButtonProps["variant"], off: ButtonProps["variant"] }, icons?: { on: string, off: string }, initialState?: boolean, children?: React.ReactNode, callback?: () => any, }) { const [isButtonOn, setIsButtonOn] = useState(initialState); const [isPending, startTransition] = useTransition(); const spanClassList = ["icon"]; if (buttonText) spanClassList.push("mr-1"); if (isPending) spanClassList.push("animate-spin"); const iconJSX = ( <span className={cn(spanClassList)}> {isPending ? "autorenew" : isButtonOn ? buttonIcons.off : buttonIcons.on} </span> ) return ( <Button variant={isButtonOn ? buttonVariants.off : buttonVariants.on} onClick={() => startTransition(async () => { setIsButtonOn(callback && await callback()) })} disabled={isPending} > {(isButtonOn ? buttonIcons.off : buttonIcons.on) && iconJSX} {buttonText} </Button> ) } This is my client component, so how would I make the text dynamic, but passed via children instead of as a prop?
the useTransition-Hook does [not allow async](https://react.dev/reference/react/useTransition#react-doesnt-treat-my-state-update-as-a-transition) function:
The function you pass to startTransition must be synchronous.

Another thing to mention: for what do you need the transition here? There won't be any blocking when clicking a button.

So: create a function that: updates your clientside state (that handles what will be displayed as button text) and await the result of your server action. Then set the clientside state to the result of your server action (successfull or not)
it's only async to allow await in the callback which is useless now I removed the dead code
I had code after setIsButtonOn() before
So ultimately, the button should have four states, which look like this