Hydration mismatch in PlateStatic due to dynamic data-slate-id
/data-block-id
attributes
#4470
Replies: 8 comments 1 reply
-
Hi @janhesters! I'm Dosu and I’m helping the plate team. This hydration mismatch happens because PlateStatic (and the node-id plugin) generates random IDs for data-slate-id and data-block-id on both server and client, but they aren't synchronized, so React sees different attributes and warns about it. There’s no built-in way in Plate 49.0.9 to disable or synchronize these IDs for SSR—this is a known limitation right now. Possible workarounds: you can inject stable IDs into your document model before rendering on both server and client, or fork/patch the node-id plugin to skip or stabilize these attributes during SSR. Another option is to strip these attributes from the server-rendered HTML, but that may break some features if you need them client-side. There was a PR introducing editor.uid and some SSR hydration support, but it doesn’t fully solve the random node ID issue for static rendering yet see PR. The attributes are not set directly in PlateStatic, but come from plugins and node properties see code. If you need deterministic SSR hydration, you’ll need to implement one of the above workarounds for now. To reply, just mention @dosu. How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other |
Beta Was this translation helpful? Give feedback.
-
Can you describe how to do these workarounds? Also, there is no way to tag @dosu. |
Beta Was this translation helpful? Give feedback.
-
Here is the new helper we've added recently. |
Beta Was this translation helpful? Give feedback.
-
Here's how you can approach each workaround: 1. Inject stable IDs into your document model: import { normalizeNodeId } from '@udecode/plate-node-id';
// Example: simple counter-based idCreator
let counter = 1;
const idCreator = () => (counter++).toString();
const stableValue = normalizeNodeId(yourValue, { idCreator });
// Pass stableValue to your editor on both server and client This ensures the IDs are always the same, preventing hydration mismatches. See NodeIdPlugin source and tests. 2. Patch or configure the node-id plugin: 3. Strip attributes from server-rendered HTML: html = html.replace(/ data-slate-id="[^"]*"/g, '').replace(/ data-block-id="[^"]*"/g, ''); Be aware: some Plate features may rely on these IDs for selection, editing, or plugins, so stripping them could break interactivity if you later hydrate the editor. Tradeoffs: Let me know if you want a more detailed code sample for your setup! To reply, just mention @dosu. How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other |
Beta Was this translation helpful? Give feedback.
-
Thank you, but even with import { createSlateEditor, normalizeNodeId, PlateStatic } from 'platejs';
import { BaseEditorKit } from '~/components/editor/editor-base-kit';
const editor = createSlateEditor({
plugins: BaseEditorKit,
value: normalizeNodeId([
{ type: 'h1', children: [{ text: 'Server-Rendered Title' }] },
{ type: 'p', children: [{ text: 'This content is rendered statically.' }] },
]),
});
// Render statically
export default function MyStaticPage() {
return <PlateStatic editor={editor} />;
} I also tried it like this: import { createId } from '@paralleldrive/cuid2';
import { createSlateEditor, normalizeNodeId, PlateStatic } from 'platejs';
import { BaseEditorKit } from '~/components/editor/editor-base-kit';
// Render statically
export default function MyStaticPage() {
const editor = createSlateEditor({
plugins: BaseEditorKit,
value: normalizeNodeId(
[
{ type: 'h1', children: [{ text: 'Server-Rendered Title' }] },
{
type: 'p',
children: [{ text: 'This content is rendered statically.' }],
},
],
{ idCreator: createId },
),
});
return <PlateStatic editor={editor} />;
} |
Beta Was this translation helpful? Give feedback.
-
The hydration mismatch still happens because, even after using To fix this, you need to:
Example: import { createSlateEditor, normalizeNodeId, PlateStatic } from 'platejs';
import { NodeIdPlugin } from '@udecode/plate-node-id';
import { BaseEditorKit } from '~/components/editor/editor-base-kit';
// Deterministic idCreator (example: simple counter)
let counter = 1;
const idCreator = () => (counter++).toString();
const value = normalizeNodeId(
[
{ type: 'h1', children: [{ text: 'Server-Rendered Title' }] },
{ type: 'p', children: [{ text: 'This content is rendered statically.' }] },
],
{ idCreator }
);
const plugins = [
...BaseEditorKit,
NodeIdPlugin.configure({ normalizeInitialValue: null, idCreator }),
];
const editor = createSlateEditor({
plugins,
value,
});
export default function MyStaticPage() {
return <PlateStatic editor={editor} />;
} If you only need static, read-only output, you can also strip Best practice: always pre-normalize with a deterministic To reply, just mention @dosu. How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other |
Beta Was this translation helpful? Give feedback.
-
This works: import { createSlateEditor, normalizeNodeId, PlateStatic } from 'platejs';
import { BaseEditorKit } from '~/components/editor/editor-base-kit';
export default function MyStaticPage() {
// 1) reset counter on *every* render (so server & client both start at 0)
let counter = 0;
const idCreator = () => String(counter++);
// 2) your raw value
const rawValue = [
{ type: 'h1', children: [{ text: 'Server-Rendered Title' }] },
{ type: 'p', children: [{ text: 'This content is rendered statically.' }] },
];
// 3) stamp in deterministic IDs
const valueWithIds = normalizeNodeId(rawValue, { idCreator });
const editor = createSlateEditor({
plugins: BaseEditorKit,
value: valueWithIds,
});
return <PlateStatic editor={editor} />;
} |
Beta Was this translation helpful? Give feedback.
-
Perfect! Resetting the counter on every render and using a deterministic idCreator with normalizeNodeId guarantees the same IDs on both server and client, so React hydration works without warnings. This is the best way to handle SSR with PlateStatic right now—just make sure any ID logic is fully deterministic and always runs the same way on both sides. There’s no official Plate doc for this pattern yet, but your approach is spot on. Let me know if you run into any other SSR edge cases! To reply, just mention @dosu. How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Description
When rendering a static Slate editor via
PlateStatic
in an SSR-enabled React Router v7 app, React logs a hydration mismatch warning. On the server, PlateStatic emitsdata-slate-id
anddata-block-id
attributes with randomly generated values; on the client those IDs differ, so React won’t patch up the DOM.Expected output
No hydration warnings. The server-rendered HTML and client-rendered HTML should have matching attributes (or omit random IDs during SSR).
Actual output
React issues a hydration mismatch error because the server’s
data-slate-id
/data-block-id
values differ from the client’s newly generated ones.Reproduction URL
No response
Reproduction steps
npx create-react-router@latest my-react-router-app
npm run dev
).Plate version
49.0.9
Slate React version
0.117.1
Screenshots
Logs
Browsers / Environment
Browser: Brave 1.79.126 (arm64, Chromium 137.0.7151.119)
OS: macOS 15.5 (24F74)
Mode: Private window, no extensions active
Beta Was this translation helpful? Give feedback.
All reactions