Description
When a plugin provides an override, any change to the prop-level overrides object passed to <Puck> causes the plugin's override subtree to fully unmount and remount - even if the plugin itself or its override components haven't changed.
Environment
Steps to reproduce
import { useEffect, useState } from "react";
import { Puck, type Config, type Plugin } from "@puckeditor/core";
const config: Config = {
components: {
Text: {
render: () => <p>Hello</p>,
},
},
};
const data = { content: [], root: { props: {} } };
const myPlugin: Plugin = {
overrides: {
puck: ({ children }) => {
useEffect(() => {
console.log("puck override MOUNTED");
return () => console.log("puck override UNMOUNTED");
}, []);
return <>{children}</>;
},
},
};
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>
Re-render ({count})
</button>
<Puck
config={config}
data={data}
plugins={[myPlugin]}
overrides={{
headerActions: ({ children }) => (
<>
{children}
<span>Count: {count}</span>
</>
),
}}
/>
</>
);
}
- Open the console
- Click the "Re-render" button
What happens
Every click logs "puck override UNMOUNTED" followed by "puck override MOUNTED". The entire subtree wrapped by the plugin's puck override is destroyed and recreated, and the editor blinks.
What I expect to happen
The plugin override stays mounted because the plugin hasn't changed.
Debugging
loadOverrides (packages/core/lib/load-overrides.ts:39-44) wraps every override in a new inline arrow function on each invocation:
const childNode = collected[overridesType];
const Comp = (props: any) =>
plugin.overrides![overridesType]!({
...props,
children: childNode ? childNode(props) : props.children,
});
collected[overridesType] = Comp;
useLoadedOverrides re-runs loadOverrides whenever the overrides or plugins prop changes, which creates a new function reference for every override:
return useMemo(() => loadOverrides({ overrides, plugins }), [plugins, overrides]);
As a result, React sees each override as a different component and unmounts and remounts the entire tree.
Any consumer that passes an override prop using a closure for rendering will trigger a full remount of all overrides on every change, even when the overrides themselves have not changed or are provided as plugins.
Description
When a plugin provides an override, any change to the prop-level overrides object passed to
<Puck>causes the plugin's override subtree to fully unmount and remount - even if the plugin itself or its override components haven't changed.Environment
Steps to reproduce
What happens
Every click logs "puck override UNMOUNTED" followed by "puck override MOUNTED". The entire subtree wrapped by the plugin's puck override is destroyed and recreated, and the editor blinks.
What I expect to happen
The plugin override stays mounted because the plugin hasn't changed.
Debugging
loadOverrides(packages/core/lib/load-overrides.ts:39-44) wraps every override in a new inline arrow function on each invocation:useLoadedOverridesre-runsloadOverrideswhenever theoverridesorpluginsprop changes, which creates a new function reference for every override:As a result, React sees each override as a different component and unmounts and remounts the entire tree.
Any consumer that passes an override prop using a closure for rendering will trigger a full remount of all overrides on every change, even when the overrides themselves have not changed or are provided as plugins.