Search code examples
reactjstypescriptnext.jsstatereact-props

State both ways between Server and Client components in Next 14


I am trying to lift state up from a client component to a server component, do some stuff on the server and pass state back back down from server to client via props.

I'm creating a booking system using Next 14, Shadcn-ui, React-hook-form etc., meaning my form has to be in a client component, and all my data fetching is in the server component.

What I want to do is have the user pick a day using a date picker, pass that to the server, which fetches bookings that day and calculates which time slots are free, and passes those time slots back down to the client to populate a combobox.

See diagram

The closest I've gotten is getting state up, but I can only get a Promise back down. This is what my code looks like.

Server Component (Parent)

export type GetAvailableSlotsType = (date: Date) => Slot[];

export default async function NewBookingsPage() {
  const getAvailableSlots: GetAvailableSlotsType = async (date) => {
    "use server";
    console.log(`Date: ${date}`) // Shows correct date
    // Do some logic
    return availableSlots
  }
  return (
    <NewBookingForm getAvailableSlots={getAvailableSlots} />
  )
}

Client Component (Child)

import { GetAvailableSlotsType } from "../page";

type NewBookingFormProps = {
  getAvailableSlots: GetAvailableSlotsType;
};

export default function NewBookingForm({
  getAvailableSlots,
}: NewBookingFormProps) {
  // Some form schema stuff

  const availableSlots = getAvailableSlots(form.watch("date"));

  return (
    <form>
      // render form
    </form>
  )

Like I say, availableSlots in the child component comes through as a promise and is unusable, despite returning in from getAvailableSlots in a nice usable object shape.

Thanks ins advance.


Solution

  • Server components are static stateless components that can be treated as pure functions designed to generate UIs without JavaScript. Children components cannot affect a parent server component in the way you describe in step 2 in your diagram.

    Also, it is not necessary for you to pass the getAvailableSlots server action to the child component, you could separate it in a file (see the client component server action convention in Next's docs) and use it directly in any component you wish by treating it as an asynchronous function (see server action used in non-form elements in Next's docs).

    In your case, as just a date needs to be passed, we can treat it completely as an async function instead of a form submitting function, in which case it is recommended to use useTransition to have the pending state (see Note at the bottom for more details).

    As you are designing an interactive component (a combobox) with asynchronous data fetching, you should not use a server component for this purpose. If you were instead displaying a static list of availabilities based on the current URL for example, a server component would fit this use-case perfectly.

    To sum up, you can replace "Server component" by "Server action" in your diagram, and it would be correct. The big mindset shift from Next13 to Next14 is that Server actions do not require Server components, so feel free to use them outside of Server-side components :)

    Here is a general refactor of your code that should work:

    Server Action

    "use server";
    export default async function getAvailableSlots(date: Date): Promise<Slot[]> {
      console.log(`Date: ${date}`) // Shows correct date
      // Do some logic
      return availableSlots
    }
    

    Client component

    import { useEffect, useState, useTransition } from "react";
    
    export default function NewBookingForm() {
      // Some form schema stuff
      const [isPending, startTransition] = useTransition();
      const [availableSlots, setAvailableSlots] = useState<Slot[]>([])
    
      const date = form.watch("date")
      useEffect(() => {
         startTransition(async () => {
            const slots = await getAvailableSlots(date);
            setAvailableSlots(slots);
         })
      }, [date]);
    
      const availableSlots = getAvailableSlots(form.watch("date"));
    
      return (
        <form>
          {/* render form */}
          {/* render combobox somewhere, you can render a loader with isPending inside the combobox */}
          <ComboBox choices={availableSlots} isPending={isPending} />
        </form>
      )
    

    If you have any more questions, feel free to ask.

    Wish you success in your project!

    (Note: a big advantage of server actions is that they can be used as native form submitting function, allowing them to work without JS enabled. By treating the server action as an async function, we lose that advantage, and as such lose some "progressive enhancement". However, the endpoint is still managed by NextJS and auto-scaled as a lambda function in Vercel for example)