puck icon indicating copy to clipboard operation
puck copied to clipboard

Expose utilities to Puck API to resolve component data on demand

Open FedericoBonel opened this issue 1 month ago • 1 comments

Closes #1249

Description

This PR adds two new functions to the Puck API to run component resolve data on demand and handle all internal state (showing loaders, checking cache, injecting params, etc.).

Changes made

  • Added a new utility function resolveAndReplaceData that runs resolve data for a component and dispatches the required replace operation afterward, handling getting the latest item selector.
  • Added a new utility function resolveDataById that runs resolve data for a component by ID using the new resolveAndReplaceData function.
  • Added a new utility function resolveDataBySelector that runs resolve data for a component by selector using the new resolveAndReplaceData function.
  • Added the new functions to the usePuck hook.
    • Changed the UsePuckData type to expect the two new functions.
    • Changed the generateUsePuck utility implementation to create the two new functions.
    • Changed the generateUsePuck utility to expect getState from the AppStoreApi as the second parameter. This was done so it can be used in the new functions and access the latest state to get the latest item selector before dispatching the required replace action to update the component with the resolved data.
  • Added tests for the new functions.

How to test

Test resolveDataById

  1. Create a config that defines two components: a Heading with a long resolveData and debugging logs, and a Filler component that acts as filler for the page.
export const conf = {
  components: {
    Filler: {
      render: () => <h1>Filler</h1>,
    },
    Heading: {
      fields: { title: { type: "text" } },
      defaultProps: { title: "Heading" },
      resolveData: async (data) => {
        const someResolveValue = await new Promise((res) =>
          setTimeout(() => res(Math.floor(Math.random() * 100)), 10000)
        );

        console.log(`Resolved for ${data.props.id}: ${someResolveValue}`);

        return {
          ...data,
          props: { ...data.props, resolved: someResolveValue },
        };
      },
      render: ({ id, title, resolved }) => (
        <div>
          <p>{id}</p>
          <h1>{title}</h1>
          <p>{resolved}</p>
        </div>
      ),
    },
  },
};
  1. Render the Puck component with a Header override that adds a button to insert a new component and resolve its data, and another one to update the Heading title by ID and resolve its data:
<Puck
  config={config}
  data={{}}
  overrides={{
    headerActions: ({ children }) => {
      const resolveDataBySelector = usePuck(
        (s) => s.resolveDataBySelector
      );
      const dispatch = usePuck((s) => s.dispatch);
      const getSelectorForId = usePuck((s) => s.getSelectorForId);

      return (
        <>
          <div style={{ display: "flex", gap: 16 }}>
            <Button
              onClick={() => {
                const newItemSelector = {
                  index: 0,
                  zone: "root:default-zone",
                };

                dispatch({
                  type: "insert",
                  componentType: "Heading",
                  destinationIndex: newItemSelector.index,
                  destinationZone: newItemSelector.zone,
                });

                resolveDataBySelector(newItemSelector);
              }}
            >
              Add component
            </Button>
            <form
              onSubmit={(e) => {
                e.preventDefault();
                const form = new FormData(e.currentTarget);
                const id = form.get("id")?.toString();
                if (!id) return;

                const selector = getSelectorForId(id);
                if (!selector) return;

                dispatch({
                  type: "replace",
                  destinationIndex: selector.index,
                  destinationZone: selector.zone,
                  data: {
                    type: "Heading",
                    props: { id, title: "Something new" },
                  },
                });

                resolveDataBySelector(selector);
              }}
              style={{ display: "flex", gap: 8 }}
            >
              <input name="id" type="text" placeholder="id" />
              <Button type="submit">Update component</Button>
            </form>
          </div>
          {children}
        </>
      );
    },
  }}
/>
  1. Navigate to the editor
  2. Open devtools
  3. Add two Filler components to the page
  4. Press the "Add component" button
  5. Move the new Heading component (while resolving) to the bottom
  6. Wait for the resolver to finish
  7. Observe that the resolved value in the console matches the one on the page

https://github.com/user-attachments/assets/3a6aaffc-1735-420e-a02d-9bcedb48e8d0

Test resolveDataBySelector

  1. Follow the steps from the previous test
  2. Copy the ID of the new Heading component from the devtools console
  3. Paste it into the field in the editor header and press "Update component"
  4. Move the new Heading component (while resolving) to the top
  5. Wait for the resolver to finish
  6. Observe that the resolved value in the console matches the one on the page

https://github.com/user-attachments/assets/101a424b-08c7-4e1b-b3e5-0944f2679ac3

Recommendations for next steps

  • There are multiple places where we dispatch replace actions after resolving data that might benefit from the new resolveAndReplaceData utility. It might be worth refactoring them to use it.
  • Console and UI messages are currently hardcoded throughout the codebase. It might be good to move them to a constants module so they are all in one place, as some are repeated. This could also help prepare for future localization features.

FedericoBonel avatar Nov 12 '25 11:11 FedericoBonel

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
puck-demo Ready Ready Preview Comment Nov 12, 2025 11:37am
puck-docs Ready Ready Preview Comment Nov 12, 2025 11:37am

vercel[bot] avatar Nov 12 '25 11:11 vercel[bot]