docs/solutions/developer-experience/2026-03-28-next-prerendered-client-editors-need-dnd-hooks-to-noop-on-the-server.md
Upgrading apps/www from [email protected] to [email protected] exposed a latent prerender failure in editor demo pages that include Plate drag-and-drop UI.
The failure was not that the app forgot to render a DndProvider. The app already wrapped relevant trees with DndProvider, but @platejs/dnd still executed react-dnd hooks during server prerender, where browser drag-and-drop does not exist.
That diagnosis was wrong.
apps/www already had DndProvider wrappers in:
apps/www/src/registry/components/editor/plugins/dnd-kit.tsxapps/www/src/components/context/providers.tsxAdding more provider plumbing would only shuffle the problem around.
dynamic(..., { ssr: false })That avoids the crash, but it is the wrong layer.
The bug lives in the package behavior, not in the route definitions. If a browser-only DnD hook cannot survive server prerender, the DnD package should degrade gracefully instead of forcing every app surface to opt out of prerender manually.
Make @platejs/dnd return inert drag/drop connectors when DOM DnD is unavailable.
Add a small environment utility:
Then guard the hook entry points:
packages/dnd/src/hooks/useDragNode.tspackages/dnd/src/hooks/useDropNode.tspackages/dnd/src/hooks/useDndNode.tsThe essential behavior is:
if (!canUseDomDnd()) {
return inertStateAndNoopConnectors;
}
That keeps drag-and-drop fully active in the browser while making prerender paths safe.
Add a regression test:
The test forces the no-DOM path and asserts that react-dnd hooks are not called.
react-dnd is a browser interaction layer. Server prerender has no real drag backend, so calling useDrag / useDrop there is a category error.
The correct contract is:
Once the package follows that contract, prerendered client editor routes stop exploding and app routes do not need ad hoc lazy-loading wrappers.
If build output says Expected drag drop context, do not assume the app forgot DndProvider.
First check whether the failing path is server prerender. If it is, the more likely issue is that a browser-only hook is being called too early.
If the same editor surface appears in multiple routes, route-level ssr: false wrappers become whack-a-mole fast. A package-level no-op path is the durable fix.
In this repo, next upgrade updated the manifest and lockfile correctly, but its install step failed on the root prepare script because of an existing skiller config problem unrelated to Next itself.
These commands passed after the fix:
bun test packages/dnd/src/components/useDraggable.spec.tsx
pnpm --filter @platejs/dnd build
pnpm --filter @platejs/dnd typecheck
pnpm -C apps/www build
pnpm -C apps/www typecheck
pnpm lint:fix