Puck 0.19 adds a lot of new features, and while updating won’t cause immediate breaking changes, some features include deprecations or behavioral differences that may affect your setup if you opt-in to them.
In this post, we will guide you through the steps for upgrading to Puck 0.19:
If you encounter any bugs as you upgrade to Puck 0.19, please report them via GitHub.
For a list of all the new features added to Puck 0.19, check out the release blog post.
Run the following command to install the latest version of Puck:
npm install --save-exact @measured/puck@^0.19.0
Puck 0.19 doesn’t introduce any breaking changes, so upgrading from Puck 0.18 won’t require updates to your existing integration.
However, if you choose to adopt the new slots
API, there are changes to the data model that could break existing saved data or custom data logic. The next section outlines what those changes are and everything you need to do to handle them safely.
DropZone
In Puck 0.19, we’re replacing DropZones with slots and deprecating the DropZone
component.
Using the DropZone
component will warn:
⚠️ DropZones have been deprecated in favor of slot fields and will be removed in a future version of Puck.
Please see the migration guide: https://www.puckeditor.com/docs/guides/migrations/dropzones-to-slots
To migrate to slots, replace your <DropZone />
instances with slot fields. We recommend using the same name for the slot field as the original zone
prop provided to your DropZone
instance to assist with data migration:
// Before
import { DropZone } from "@measured/puck";
const config = {
components: {
Flexbox: {
render: () => {
return <DropZone zone="FlexZone" style={{ display: "flex" }} />;
},
},
},
};
// After
const config = {
Flexbox: {
fields: {
// Match this name (FlexZone) to your previous zone prop
FlexZone: { type: "slot" },
},
render: ({ FlexZone }) => {
// Replace the DropZone with the slot
return <FlexZone style={{ display: "flex" }} />;
},
},
};
puck.renderDropzone
Since slots work in both client and server components, puck.renderDropzone
is being replaced by slots and is now deprecated.
Using puck.renderDropzone
will warn:
⚠️ DropZones have been deprecated in favor of slot fields and will be removed in a future version of Puck.
Please see the migration guide: https://www.puckeditor.com/docs/guides/migrations/dropzones-to-slots
To migrate to slots, replace your puck.renderDropzone
calls with slot fields. We recommend using the same name for the slot field as the original zone
prop provided to your puck.renderDropzone
call to assist with data migration:
// Before
const config = {
components: {
Flexbox: {
render: ({ puck }) => {
return (
<div>
{puck.renderDropzone({
zone: "FlexZone",
style: { display: "flex" },
})}
</div>
);
},
},
},
};
// After
const config = {
Flexbox: {
fields: {
// Match this name (FlexZone) to your previous zone prop
FlexZone: { type: "slot" },
},
render: ({ FlexZone }) => {
// Replace the puck.renderDropzone call with the slot
return <FlexZone style={{ display: "flex" }} />;
},
},
};
data.zones
When using Slots, nested components are stored recursively as props against the component that defines the slot, instead of as zones
in the data payload. If you’re migrating from DropZones to slots, you must update any existing data.
Before:
{
"content": [
{
"type": "Flexbox",
"props": {
"id": "Flexbox-12345"
}
}
],
"zones": {
"Flexbox-12345:FlexZone": [
{
"type": "HeadingBlock",
"props": {
"id": "Heading-12345",
"title": "This is a nested heading"
}
}
]
}
}
After:
{
"content": [
{
"type": "Flexbox",
"props": {
"id": "Flexbox-12345",
"FlexZone": [
{
"type": "HeadingBlock",
"props": {
"id": "Heading-12345",
"title": "This is a nested heading"
}
}
]
}
}
]
}
To update your data, use the migrate
utility. This function will convert all DropZone
data stored under data.zones
into recursive slots. It takes two arguments: the legacy data object, and the current config
with slot definitions.
For the migration to work, make sure the field name of each slot in your config matches the zone
prop that was used in the DropZone
it replaces.
import { migrate } from "@measured/puck";
const newConfig = {
Flexbox: {
fields: {
FlexZone: { type: "slot" },
},
render: ({ FlexZone }) => {
return <FlexZone style={{ display: "flex" }} />;
},
},
};
const legacyData = {
root: {},
content: [
{
type: "Flexbox",
props: {
id: "Flexbox-12345",
},
},
],
zones: {
"Flexbox-12345:FlexZone": [
{
type: "HeadingBlock",
props: {
id: "Heading-12345",
title: "Header",
},
},
],
},
};
const newData = migrate(legacyData, newConfig);
// {
// "content": [
// {
// "type": "Flexbox",
// "props": {
// "id": "Flexbox-12345",
// "FlexZone": [
// {
// "type": "HeadingBlock",
// "props": {
// "id": "Heading-12345",
// "title": "Header"
// }
// }
// ]
// }
// }
// ]
// }
If you’re manually traversing or modifying the data payload, you’ll need to account for the data model change when using slots.
To do this, you can use the new walkTree
utility. This function recursively walks your entire data payload, and allows for optional updates.
// Before
const processContent = (content) => {
// Add the "example" prop to all children
return content.map((child) => ({
...child,
props: { ...child.props, example: "Hello, world" },
}));
};
const processedData = data;
// Update `data.content`
processedData.content = processContent(data.content);
// Update `data.zones`
processedData.zones = Object.keys(data.zones).reduce((newZones, zoneKey) => {
const nestedComponents = data.zones[zoneKey];
return {
...newZones,
[zoneKey]: processContent(nestedComponents),
};
}, {});
// After
import { walkTree } from "@measured/puck";
const processedData = walkTree(data, config, (nestedComponents) => {
// Add the "example" prop to all children
return nestedComponents.map((child) => ({
...child,
props: { ...child.props, example: "Hello, world" },
}));
});
createUsePuck
and useGetPuck
Before 0.19, using usePuck
subscribed your component to the entire internal Puck API, causing it to trigger re-renders on every state change. To avoid that, Puck now provides two utilities: createUsePuck
and useGetPuck
.
createUsePuck
createUsePuck
is a hook factory that lets you create a version of usePuck
that supports selectors, allowing you to specify which parts of the state or Puck API you want to react to.
We recommend using this approach whenever you want to use a part of the Puck state to render your component:
// Before
import { usePuck } from "@measured/puck";
const PreviewModeIndicator = () => {
// Re-renders on every appState change
const { appState } = usePuck();
return <p>{appState.ui.previewMode}</p>;
};
// After
import { createUsePuck } from "@measured/puck";
const usePuck = createUsePuck();
const PreviewModeIndicator = () => {
// Re-renders only when preview mode changes
const previewMode = usePuck((s) => s.appState.ui.previewMode);
return <p>{previewMode}</p>;
};
useGetPuck
useGetPuck
returns a getter function that gives you access to the latest Puck API without subscribing to changes, meaning your component won’t re-render because of it.
We recommend using this approach whenever possible, such as in callbacks:
// Before
import { usePuck } from "@measured/puck";
const SaveDataButton = () => {
// Re-renders on every appState change
const { appState } = usePuck();
const handleClick = () => saveData(appState.data);
return <button onClick={handleClick}>Save your progress</button>;
};
// After
import { useGetPuck } from "@measured/puck";
const SaveDataButton = () => {
const getPuck = useGetPuck();
const handleClick = useCallback(() => {
// Only get the appState when the button is clicked
const { appState } = getPuck();
saveData(appState.data);
}, [getPuck]);
return <button onClick={handleClick}>Save your progress</button>;
};
appState.ui.itemSelector.zone
The zone
parameter on the item selector has been updated to support slots.
This parameter is a concatenated string, made up of the parent id (that defines the DropZone or slot) and an identifier for that particular zone (the DropZone zone
prop, or name of the slot field).
// Before
const config = {
Example: {
render: () => {
return <DropZone zone="myZone" />;
},
},
};
console.log(appState.ui.itemSelector.zone);
// "Example-1234:myZone"
// After
const config = {
Example: {
fields: {
mySlot: {
type: "slot",
},
},
render: ({ mySlot: MySlot }) => {
return <MySlot />;
},
},
};
console.log(appState.ui.itemSelector.zone);
// "Example-1234:mySlot"
This change only affects you if you were using itemSelector.zone
to look up content in data.zones
. The new getItemBySelector
method provided by the PuckApi
will enable you to migrate this behavior:
// Before
import { usePuck } from "@measured/puck";
const selector = { zone: "Example-1234:myZone", index: 0 };
const LogItemButton = () => {
const { appState } = usePuck();
return (
<button
onClick={() => {
console.log(appState.data.zones[selector.zone][selector.index]);
}}
>
Announce item
</button>
);
};
// After
import { useGetPuck } from "@measured/puck";
const selector = { zone: "Example-1234:mySlot", index: 0 };
const LogItemButton = () => {
// Also migrate to useGetPuck to prevent re-renders
const getPuck = useGetPuck();
return (
<button
onClick={() => {
const { getItemBySelector } = getPuck();
console.log(getItemBySelector(selector));
}}
>
Announce item
</button>
);
};
Check out the full changelog, including known issues, on the GitHub release.
If you’re interested in learning more about Puck, check out the demo or read the docs. If you like what you see, please give us a star on GitHub to help others find Puck too!