-
Notifications
You must be signed in to change notification settings - Fork 652
fix issues with navlist static to interactive subnav state #7511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix issues with navlist static to interactive subnav state #7511
Conversation
🦋 Changeset detectedLatest commit: b8994ab The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Fixes a NavList flicker where a parent item with a SubNav briefly renders in the wrong expanded/current state when transitioning from static (SSR markup) to interactive (hydrated) state.
Changes:
- Add a recursive pre-render check to detect whether a SubNav contains the current item and initialize the parent’s open state accordingly.
- Add an SSR-focused unit test to verify initial expanded rendering when a current SubNav item exists.
- Add a dev Storybook scenario that reproduces the static-to-interactive transition flicker.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| packages/react/src/NavList/NavList.tsx | Pre-computes “contains current item” before first render to avoid initial-state mismatch/flicker. |
| packages/react/src/NavList/NavList.test.tsx | Adds SSR test to validate initial expanded state when SubNav contains the current item. |
| packages/react/src/NavList/NavList.dev.stories.tsx | Adds an amplified Storybook repro by switching between static HTML and interactive render. |
| @@ -1,4 +1,6 @@ | |||
| import type {Meta} from '@storybook/react-vite' | |||
| import React, {useEffect} from 'react' | |||
| import {renderToStaticMarkup} from 'react-dom/server' | |||
Copilot
AI
Feb 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Importing renderToStaticMarkup from react-dom/server in a Storybook story (browser bundle) can break Vite/Storybook builds because react-dom/server is Node-oriented. Prefer switching this import to react-dom/server.browser (if available in your React version), or restructure the story to avoid bundling the server renderer into the client (e.g., generate the static markup outside the browser bundle / via a build-time step).
| import {renderToStaticMarkup} from 'react-dom/server' | |
| import {renderToStaticMarkup} from 'react-dom/server.browser' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove the .dev.stories after a maintainer has tested the changes locally. It's just to showcase the bug
| const staticMarkup = React.useMemo(() => { | ||
| return renderToStaticMarkup(<StaticTransitionNav currentItem={currentItem} />) | ||
| }, [currentItem]) |
Copilot
AI
Feb 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Importing renderToStaticMarkup from react-dom/server in a Storybook story (browser bundle) can break Vite/Storybook builds because react-dom/server is Node-oriented. Prefer switching this import to react-dom/server.browser (if available in your React version), or restructure the story to avoid bundling the server renderer into the client (e.g., generate the static markup outside the browser bundle / via a build-time step).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, we can remove the .dev.stories after a maintainer has tested the changes locally. It's just to showcase the bug
| it('renders parent item expanded on initial static render when SubNav contains the current item', () => { | ||
| const markup = renderToStaticMarkup(<NavListWithCurrentSubNav />) | ||
| expect(markup).toContain('aria-expanded="true"') | ||
| }) |
Copilot
AI
Feb 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assertion isn’t scoped to the parent item that should be expanded (it will pass if any aria-expanded="true" exists anywhere in the markup). Consider parsing the markup into a DOM container and asserting aria-expanded on the specific control associated with “Item 2” (e.g., query the toggle button by accessible name / nearby text, then assert its aria-expanded value).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about it, but the test works to test what we want atm. I don't want to overcomplicate the test too much. An additional thought about this, is that maybe we should supress this warning:
Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes.%s
at ItemWithSubNav (http://localhost:63315/src/NavList/NavList.tsx:210:13)
at http://localhost:63315/src/NavList/NavList.tsx:75:13
at ul
at UnwrappedList (http://localhost:63315/src/ActionList/List.tsx:15:9)
at nav
at http://localhost:63315/src/NavList/NavList.tsx:17:13
at NavListWithCurrentSubNav (http://localhost:63315/Users/rso/repos/personal/primer-react/packages/react/src/NavList/NavList.test.tsx?import&browserv=1770504860076:189:15)
Using a consoleSpy and just ignoring the output like vi.spyOn(console, 'error').mockImplementation(() => null) ? I didn't do it because I'm not entirely sure if its bad practise or not.
Changelog
This PR fixes a visual flicker in NavList where a parent item with SubNav could briefly appear in the wrong state when navigating between sibling sub-items.
This can be seen here (example happening on the docs site
primer.style):example.from.docs.mov
Notice how
Foundationsflicker when I change submenu.Here is a minimal example (amplified)
amplified.example.mov
I updated the dev storybook with an amplified example, that makes it easier to see whats actually happening.
Example after fix
postfix.mov
This is how it looks after the fix.
Rollout strategy
Testing & Reviewing
Just replace these parts:
With the "current" behaviour:
and test in storybook: http://localhost:6006/?path=/story/components-navlist-dev--sub-nav-static-to-interactive-transition
Merge checklist