fix(INSTUI-5152): fix SSR hydration with stable id generation - #2694
fix(INSTUI-5152): fix SSR hydration with stable id generation#2694HouseOfHawks wants to merge 1 commit into
Conversation
6bea6ea to
fe7ea08
Compare
|
Visual regression reportCypress suite: ✅ Passing Visual diff: ✅ No changes.
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 |
fe7ea08 to
5b31da1
Compare
| "lint-staged": { | ||
| "*.{js,ts,tsx}": [ | ||
| "oxlint -c .oxlintrc.json --fix", | ||
| "oxlint -c .oxlintrc.json --fix --no-error-on-unmatched-pattern", |
There was a problem hiding this comment.
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"]', |
There was a problem hiding this comment.
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.
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
5b31da1 to
c8ca412
Compare
This PR:
ui-utilspackage,deterministicIdContextandgenerateIdfunction in lieu of the newuseDeterministicIdfunctional hook orwithDeterministicIddecorator,Known Issue - 1 (not a regression and not fixed in this PR):
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