How to combine Custom Behavior and DataProvider in code component

I have a custom behavior component, which allows a Plasmic Studio user to give any button a special onClick handler. The onClick handler sends a network request and tracks an isLoading state in the component.

How do I expose isLoading to the Plasmic Studio user so they can use it in dynamic values. e.g. to conditionally render the button text depending on the loading state.

I have tried wrapping the custom behavior component in a <DynamicProvider> component so that I can expose isLoading to the button. Doing this, I can succesfully access this dynamic value in the Studio, but the custom onClick behavior is no longer triggered. I am probably doing something wrong here. Does anyone have an example of how best to implement this?

Please see my custom behaviour code component below:

import { apiClient } from "@/services/apiClient";
import { DataProvider } from "@plasmicapp/host";
import { useMutation } from "@tanstack/react-query";
import React, { ReactElement, cloneElement, useEffect } from "react";
import type {
  BookingCheckoutResponse,
  CreateBookingPayload,
  StripeButtonProps,
} from "./types";

async function createBooking(data: CreateBookingPayload) {
  const res = await apiClient.post("/bookings/checkout", data);
  return res.data;
}

function StripeBookingButton({
  children,
  bookerId,
  customers,
  group_coupon,
  locationId,
  start,
  payInFull,
  onLoading,
  ...props
}: StripeButtonProps) {
  const payload = {
    booking: {
      auto_duration: true,
      booker: bookerId,
      customers: customers,
      end: null,
      group_coupon: group_coupon,
      location: locationId,
      source: "Online",
      start: start,
      tos_accepted_at: new Date().toISOString(),
    },
    pay_in_full: payInFull,
    provider: "stripe",
  };

  // For composability in Plasmic Studio
  const filteredProps = Object.fromEntries(
    Object.entries(props).filter(
      ([key]) => !key.startsWith("data-plasmic") && key !== "className"
    )
  );

  const newBooking = useMutation(
    (payload: CreateBookingPayload) => createBooking(payload),
    {
      onError: (err: any) => {
        if (err.response.status === 409) {
          // handleSlotNotAvailable();
          console.log("Slot not available");
        }
      },
    }
  );

  const handleCheckout = async () => {
    console.log("Handling Checkout...");
    await newBooking.mutateAsync(payload);
  };

  useEffect(() => {
    if (newBooking.isSuccess) {
      const data = newBooking.data as BookingCheckoutResponse;
      if (data.amount_total === 0 || data.amount_owing === 0) {
        window.location.href =
          process.env.NEXT_PUBLIC_BOOKING_SUCCESS_URL || "/";
      } else {
        if (data.booking_id && data.booking_success) {
          window.location.href = data.url;
        }
      }
    }
  }, [newBooking.data, newBooking.isSuccess]);

  return (
    <DataProvider name="stripeBookingButtonState" data={newBooking}>
      <React.Fragment>
        {React.Children.map(children, (child) =>
          cloneElement(child as ReactElement, {
            ...filteredProps, // forward extra props for composability
            onClick: handleCheckout,
          })
        )}
      </React.Fragment>
    </DataProvider>
  );
}

export default StripeBookingButton;

Hi! When content is passed though a slot for a code component that providesData via the DataProvider, we have to do some additional wrapping so that slot content can read the context data. Suppose you’re passing in <button>{$ctx.isLoading}</button> into the slot. While you might think it looks like this:

<StripeBookingButton>
  <button>{$ctx.isLoading}</button>
</StripeBookingButton>

It actually looks like this:

<StripeBookingButton>
  <DataCtxReader>
    {($ctx) => <button>$ctx.isLoading</button>}
  </DataCtxReader>
</StripeBookingButton>

which is necessary to read the isLoading that you provided via <DataProvider/>. So, unfortunately the children is not what you expected…

One possibility is for StripeBookingButton to itself render the <button/> so it can install the click handler the right way. Another possibility is for StripeBookingButton to also provide handleCheckout via <DataProvider />, and in the Studio, for you to also set the onClick prop of the <button/> to $ctx.handleCheckout.

Hi @chungwu! Thank you so much for the explanation and possible solutions. I now have a better understanding of Plasmic internals plus React.

I went with the 2nd option you suggested in this case. Works like a charm! :smiley: