fix(ui): resolve SSR hydration mismatch in DraggableSortable (aria-describedby)#16171
Conversation
useId() generates different values between server and client when DraggableSortable is mounted inside a subtree that was not part of the initial SSR output (e.g. a hidden column-picker portal in list views). @dnd-kit derives aria-describedby on every sortable item from the DndContext id, so any server/client id mismatch triggers a React hydration warning for every draggable pill. Fix: initialise both ids through useState so they are only ever produced on the client side and remain stable across re-renders. Closes #<issue>
|
Can you verify you're getting SSR issues using one of the supported Next.js versions? This used to be a React bug that was present in older Next.js versions. If this is addressed by bumping Next.js, we won't need this change |
|
@AlessioGr Thanks for the review. I've verified this on Next.js 16.2.1 (current supported version). The hydration mismatch isnt a Next.js version bug, it's a React + dnd-kit integration pattern issue. The problem occurs when:
This is documented in the dnd-kit repo: clauderic/dnd-kit#926 The fix (useState(useId())) ensures the ID is generated once on the client, keeping SSR and hydration in sync. This pattern is the recommended solution per dnd-kit maintainers and works across all Next.js versions with SSR. Let me know if you'd like me to add a test case demonstrating the warning. |
Problem
When
DraggableSortableis mounted inside a subtree that was not part of the initial SSR output (e.g. the hidden column-picker panel in collection list views),useId()produces different values between the server render and the client hydration pass.@dnd-kitderivesaria-describedbyon every sortable item from theDndContextid. Because the id differs, React emits a hydration mismatch warning for every draggable pill in the selector:This affects all collection list views that use the column picker (Videos, Offers, Reviews, etc.).
Root cause
useId()is deterministic only when the component tree rendered on the server matches the tree hydrated on the client. When a component is conditionally mounted (hiddendiv, portal, lazy panel), the hook's counter is incremented differently on each side, producing mismatched ids.Fix
Initialise both
dndContextIDandsortableContextIDthroughuseStateinstead of using theuseId()value directly.useStateinitialises only on the client, so the id is never included in SSR output and there is nothing to mismatch.The values remain stable across re-renders (empty deps in
useState), so drag-and-drop behaviour is unaffected.References