builder
builder copied to clipboard
React 18 `hydrateRoot` hydration failed with SSR
Hey friends! Big fan of builder! I am running into an issue with builder and React 18. I am using Remix and Tailwind but I am very certain that it's React 18 that is causing the issue.
Describe the bug
React throws the following error when builder is used with React 18 and server-side rendering:
Warning: React does not recognize the
builderStateprop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase
builderstate instead. If you accidentally passed it from a parent component, remove it from the DOM element.
The server-side rendered HTML loads fine (builder page looks as expected and builder style tags present). After hydration, React reports: Hydration failed because the initial UI does not match what was rendered on the server.
Then the builder pages lose their styling (the emotion style tags disappear).
To Reproduce
Steps to reproduce the behavior:
- Use React 18 and use
hydrateRoot
- Load builder both server and client-side
- Throttle to slow 3G for better visibility
- See how the builder page breaks after attempted hyrdation
My (Remix.run) client entry file:
import { RemixBrowser } from '@remix-run/react';
import { hydrateRoot } from 'react-dom/client';
import { registerComponents } from './modules/builder-pages/registerComponents';
// registering builder components and calling builder init with public key
registerComponents();
// using React 18 hydrateRoot
hydrateRoot(document, <RemixBrowser />);
Expected behavior
Builder works as expected after React hydration and doesn't loose the CSS properties.
The following non-React 18 client entry file works:
import { RemixBrowser } from '@remix-run/react';
import { hydrate } from 'react-dom';
import { registerComponents } from './modules/builder-pages/registerComponents';
registerComponents();
hydrate(<RemixBrowser />, document);
Additional context
- React v18.2.0
- Remix.run v1.6.5
Hi @andrelandgraf , thanks for the kind words and reporting this issue, the builderState
prop is passed to all registered custom components, so this is possibly related to what you have in the registerComponents
method, please share details of this method or a codesandbox/repo I can reproduce this issue on
Hi @teleaziz, thanks for your support!
Sure,registerComponents
:
import { Builder } from '@builder.io/react';
import { CallToActionLink } from '~/components/cta/callToActionLink';
import { StartBanner } from '~/components/start/start';
import { ButtonLink, TextLink, Image, H1, H2, H3, H4, Paragraph, ImageWithLink } from '~/components/UI';
import { getPublicBuilderInstance } from './builder';
let componentsHaveBeenRegistered = false;
export function registerComponents() {
if (componentsHaveBeenRegistered) {
return;
}
componentsHaveBeenRegistered = true;
// init builder
getPublicBuilderInstance();
Builder.registerComponent(ButtonLink, {
name: 'Button Link',
inputs: [
{ name: 'children', type: 'text', defaultValue: 'Button Text', required: true, friendlyName: 'Text' },
{ name: 'to', type: 'text', defaultValue: '/', required: true, friendlyName: 'Link' },
{ name: 'primary', type: 'boolean', defaultValue: false, friendlyName: 'Is Primary Button?' },
],
});
Builder.registerComponent(TextLink, {
name: 'Link in Text',
inputs: [
{ name: 'children', type: 'text', defaultValue: 'Link Text', required: true, friendlyName: 'Text' },
{ name: 'to', type: 'text', defaultValue: '/', required: true, friendlyName: 'Link' },
],
});
Builder.registerComponent(Image, {
name: 'Cloudinary Image',
inputs: [
{
name: 'src',
type: 'text',
defaultValue: 'https://res.cloudinary.com/...',
required: true,
},
{
name: 'alt',
type: 'text',
defaultValue: '...',
required: true,
},
{
name: 'width',
type: 'text',
defaultValue: 'auto',
enum: ['auto', 'max'],
},
],
});
Builder.registerComponent(ImageWithLink, {
name: 'Cloudinary Image with Link',
inputs: [
{
name: 'src',
type: 'text',
defaultValue: 'https://res.cloudinary.com/...',
required: true,
},
{
name: 'alt',
type: 'text',
defaultValue: '...',
required: true,
},
{
name: 'width',
type: 'text',
defaultValue: 'auto',
enum: ['auto', 'max'],
},
{
name: 'ariaLabel',
type: 'text',
defaultValue: 'Link Label',
required: true,
friendlyName: 'Aria Label',
},
{ name: 'to', type: 'text', defaultValue: '/', required: true, friendlyName: 'Link' },
{ name: 'withShadow', type: 'boolean', defaultValue: true, friendlyName: 'Background Shadow' },
],
});
Builder.registerComponent(H1, {
name: 'Heading 1',
inputs: [
{ name: 'children', type: 'text', defaultValue: 'Heading 1', required: true, friendlyName: 'Text' },
{ name: 'asH2', type: 'boolean', defaultValue: false, friendlyName: 'Als Heading 2' },
],
});
Builder.registerComponent(H2, {
name: 'Heading 2',
inputs: [{ name: 'children', type: 'text', defaultValue: 'Heading 2', required: true, friendlyName: 'Text' }],
});
Builder.registerComponent(H3, {
name: 'Heading 3',
inputs: [{ name: 'children', type: 'text', defaultValue: 'Heading 3', required: true, friendlyName: 'Text' }],
});
Builder.registerComponent(H4, {
name: 'Heading 4',
inputs: [{ name: 'children', type: 'text', defaultValue: 'Heading 4', required: true, friendlyName: 'Text' }],
});
Builder.registerComponent(StartBanner, {
name: 'StartBanner',
inputs: [],
});
Builder.registerComponent(CallToActionLink, {
name: 'CallToActionLink',
inputs: [],
});
// overrides Text builder basics component
Builder.registerComponent(Paragraph, {
name: 'Text',
inputs: [
{
name: 'children',
type: 'richText',
defaultValue: '<b>Paragraph Text</b>',
required: true,
friendlyName: 'Text',
},
{
name: 'parseAsHTML',
type: 'boolean',
defaultValue: true,
showIf: () => false,
},
{
name: 'As',
type: 'text',
defaultValue: 'div',
showIf: () => false,
},
],
});
Builder.registerComponent(Paragraph, {
name: 'Paragraph',
inputs: [
{
name: 'children',
type: 'richText',
defaultValue: '<b>Paragraph Text</b>',
required: true,
friendlyName: 'Text',
},
{
name: 'parseAsHTML',
type: 'boolean',
defaultValue: true,
showIf: () => false,
},
{
name: 'As',
type: 'text',
defaultValue: 'div',
showIf: () => false,
},
],
});
}
And getPublicBuilderInstance
:
let isInitialized = false;
export function getPublicBuilderInstance() {
if (!isInitialized) {
isInitialized = true;
// init builder page rendering with public key
builder.init('...');
}
return builder;
}