Skip to content

fix(INSTUI-5152): fix SSR hydration with stable id generation - #2694

Open
HouseOfHawks wants to merge 1 commit into
masterfrom
INSTUI-5152
Open

fix(INSTUI-5152): fix SSR hydration with stable id generation#2694
HouseOfHawks wants to merge 1 commit into
masterfrom
INSTUI-5152

Conversation

@HouseOfHawks

Copy link
Copy Markdown
Collaborator

This PR:

  • Fixes ID Generation in place in ui-utils package,
  • Deprecates deterministicIdContext and generateId function in lieu of the new useDeterministicId functional hook or withDeterministicId decorator,
  • Enables strict mode in the regression test app,
  • Disables no SRR hook in the regression test app, and
  • Updates documentation regarding SSR guidance and best practices for consumers.

Known Issue - 1 (not a regression and not fixed in this PR):

<div> aria-describedby="Selectable___Rqfeminebn3alb-description" — pre-existing since 2019 (de742b4623): Selectable.getRootProps/getTriggerProps always sets aria-describedby, but Drilldown deliberately does not render getDescriptionProps

Example SSR loading/hydration in Next.js app router (results: clean hydration by components with previous breakage)

Screen.Recording.2026-08-21.at.9.57.29.AM.mov

@HouseOfHawks
HouseOfHawks force-pushed the INSTUI-5152 branch 2 times, most recently from 6bea6ea to fe7ea08 Compare August 21, 2026 14:30
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://instructure.design/pr-preview/pr-2694/

Built to branch gh-pages at 2026-08-21 15:59 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

github-actions Bot pushed a commit that referenced this pull request Aug 21, 2026
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Visual regression report

Cypress suite: ✅ Passing

Visual diff:No changes.

Status Count
Unchanged 96
Changed 0
New 0
Removed 0

Accessibility (axe): ✅ No violations.

📊 View full report — click a screenshot's ⚠ badge to see each violation boxed on the image, with the offending element named and contrast failures shown as color swatches.

Baselines come from the visual-baselines branch. They refresh on every merge to master. The Cypress suite line covers the a11y and console-error assertions — a ❌ there means the suite found real issues even if the visual diff is clean.

Comment thread package.json
"lint-staged": {
"*.{js,ts,tsx}": [
"oxlint -c .oxlintrc.json --fix",
"oxlint -c .oxlintrc.json --fix --no-error-on-unmatched-pattern",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This was throwing an error when a commit only changed the /regression-test/cypress/e2e/spec.cy.ts file - happy to pivot if necessary.

wait: 300,
// The menu is open on load (`defaultShow`), so it is server rendered open
// but unfocused until hydration applies the initial highlight.
awaitFocused: '[role="menuitem"]',

@HouseOfHawks HouseOfHawks Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem: We were hitting a visual regression with this component regarding focus state.

Technical Explanation: For real users, a real but bounded consequence of SSR. In most cases, menus are tied to an anchor element which opens a menu on click.

Details: An SSR-rendered defaultShow menu is genuinely unfocused for one hydration window (~120ms locally, longer on slow devices or the CI pipeline). No library can avoid that: focus requires JS. It's the same class of tradeoff as any interactive state that can't be expressed in HTML. Worth knowing, not worth blocking on and note this only affects menus that start open on load; the ordinary click-to-open path is unaffected.

Solution Explanation: This focus gate is an assertion, not a sleep. It retries until the settled state is real.

github-actions Bot pushed a commit that referenced this pull request Aug 21, 2026
Replaces the global instance-counter map behind useDeterministicId and
withDeterministicId with React's built-in useId, so ids are identical between the
server and the client render and no longer depend on module-level render order.
DeterministicIdContext and its instanceCounterMap are kept as deprecated no-op
exports.

Call sites that deferred id assignment to a useEffect (to dodge hydration
mismatches) now read the id during render. TopNavBar derives the custom popover
page id from the item's own id instead of a local counter.

Ids keep their historical `ComponentName___token` shape; only the source of the
token changes (from a counter to useId). The delimiters React puts around useId
values (`:r0:` in React 18, `«r0»` in React 19) are stripped, since they are not
valid in a CSS selector. withDeterministicId now delegates to useDeterministicId
so the two cannot drift.

Apps that mount multiple React roots on one page should pass a distinct
identifierPrefix to each root to keep ids unique across roots.

feat(INSTUI-5152): enhance SSR hydration with stable id generation

fix(INSTUI-5152): type fix

fix(INSTUI-5152):  regression test

fix(INSTUI-5152): ssr test config
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.

1 participant