Skip to content

fix(ui): resolve SSR hydration mismatch in DraggableSortable (aria-describedby)#16171

Open
devinoldenburg wants to merge 1 commit intopayloadcms:mainfrom
devinoldenburg:fix/draggable-sortable-ssr-hydration-mismatch
Open

fix(ui): resolve SSR hydration mismatch in DraggableSortable (aria-describedby)#16171
devinoldenburg wants to merge 1 commit intopayloadcms:mainfrom
devinoldenburg:fix/draggable-sortable-ssr-hydration-mismatch

Conversation

@devinoldenburg
Copy link
Copy Markdown

Problem

When DraggableSortable is 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-kit derives aria-describedby on every sortable item from the DndContext id. Because the id differs, React emits a hydration mismatch warning for every draggable pill in the selector:

+ aria-describedby="_R_2qjakuatpesneb6jd5rlb_"
- aria-describedby="_R_badajpbn5ritpcqdl5rlb_"

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 (hidden div, portal, lazy panel), the hook's counter is incremented differently on each side, producing mismatched ids.

Fix

Initialise both dndContextID and sortableContextID through useState instead of using the useId() value directly. useState initialises only on the client, so the id is never included in SSR output and there is nothing to mismatch.

const reactId = useId()
const [dndContextID] = useState(reactId)
const [sortableContextID] = useState(reactId + '-sortable')

The values remain stable across re-renders (empty deps in useState), so drag-and-drop behaviour is unaffected.

References

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>
@AlessioGr
Copy link
Copy Markdown
Member

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 AlessioGr self-assigned this Apr 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants