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 {
} from "./types";

async function createBooking(data: CreateBookingPayload) {
  const res = await"/bookings/checkout", data);

function StripeBookingButton({
}: 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(
      ([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 = 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.isSuccess]);

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

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:


It actually looks like this:

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

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: