Expose `resolveComponentData` to support resolving data after inserting components via `dispatch`
Description
When using dispatch:{ type: "insert, ... to add new component, resolveData is not being triggered.
Environment
- Puck version: 0.19.3 (first noticed), also on 0.20.0
- Browser version: Chrome 139 (desktop)
- Additional environment info: reproducible with
npx create-puck-app my-app
Steps to reproduce
-
Create new Puck application (I used
npx create-puck-app my-app) -
Add
resolveDatainpuck.config.tsx
import type { Config } from "@measured/puck";
type Props = {
HeadingBlock: { title: string };
};
export const config: Config<Props> = {
components: {
HeadingBlock: {
fields: {
title: { type: "text" },
},
defaultProps: {
title: "Heading",
},
resolveData(data, params) {
console.log("resolveData", params);
return data;
},
render: ({ title }) => (
<div style={{ padding: 64 }}>
<h1>{title}</h1>
</div>
),
},
},
};
export default config;
- Add a magic button for inserting new components in
app/puck/[...puckPath]/client.tsx
"use client";
import type { Data } from "@measured/puck";
import { Puck, createUsePuck } from "@measured/puck";
import config from "../../../puck.config";
export function Client({ path, data }: { path: string; data: Partial<Data> }) {
return (
<Puck
config={config}
data={data}
onPublish={async (data) => {
await fetch("/puck/api", {
method: "post",
body: JSON.stringify({ data, path }),
});
}}
overrides={{
outline: CustomAdd,
}}
/>
);
}
const useGetPuck = createUsePuck();
export const CustomAdd = () => {
const dispatch = useGetPuck((s) => s.dispatch);
const handleClick = () => {
dispatch({
type: "insert",
componentType: "HeadingBlock",
destinationIndex: 0,
destinationZone: "root:default-zone",
});
};
return <button onClick={handleClick}>Add component</button>;
};
- Run the application in development mode, go to
editpage and use magic button for adding the component
What happens
New component is being added, but resolveData is not executed
What I expect to happen
resolveData should be called like it is when we add new block via drag and drop
Additional Media
https://github.com/user-attachments/assets/976e27e8-3f7c-4f5b-bcdb-944bd983fc73
Hey @SlawomirMazgaj! Thanks for reporting this. I initially thought it was a bug, but it turns out this is actually intended behavior with actions, since they’re meant to be atomic.
Inside the Puck code, when a new component is added, it dispatches an insert action and then imperatively calls its resolveData. Once the resolver returns, it sends a replace action to update the component’s data.
This is designed to allow asynchronous resolveData logic without blocking UI feedback, users expect to see the new component appear immediately in the canvas, even if the resolver is still running.
So currently, if you want to resolve data when inserting a component programmatically, you need to use resolveAllData to run all resolvers and then dispatch the updated data with a setData action. Alternatively, you could locate the new component in your resolved data and dispatch a replace action to update just that component’s data.
That said, this is definitely a bit cumbersome. After discussing with the team, we agreed it would be helpful to expose the internal utility we use to resolve data for a single component. I’ll keep this issue open to track the request for exposing that utility to better support this use case.
Hi @FedericoBonel! Thank you for looking into that and for the explanation. It makes sense now resolveData is not being triggered after dispatching insert action.
I was just trying to use resolveAllData, but I'm not sure I fully understand it. I'm guessing it just invokes, the resolveData on each component, but what data should I pass as a first parameter?
I think in my specific use case I don't have any async resolveData to await, just need to override some data based on the component type, so I can probably get away with dispatching replace action right after.
Alternatively, you could locate the new component in your resolved data and dispatch a replace action to update just that component’s data.
That would require calling resolveAllData, right? Can I also get component's data some other way after dispatching insert action?
Hey @SlawomirMazgaj!
Yes, the data you would pass is the appState.data, which represents your entire editor data.
I think in my specific use case I don't have any async resolveData to await, just need to override some data based on the component type, so I can probably get away with dispatching replace action right after.
If you only need to override some data statically, then yes! You can "simulate" data resolving by dispatching a replacement action with the payload you need.
Another option would be to call config.components[componentType].resolveData with your default props as the first parameter and some simulated params as the second, just to let it resolve the data. But that only makes sense if you really need it, which does not seem to be the case here.