Skip to content

Fix TOC groups losing expand/active state during client-side navigation (RND-11931)#4404

Draft
zenoachtig wants to merge 2 commits into
mainfrom
claude/tender-allen-0t2uwu
Draft

Fix TOC groups losing expand/active state during client-side navigation (RND-11931)#4404
zenoachtig wants to merge 2 commits into
mainfrom
claude/tender-allen-0t2uwu

Conversation

@zenoachtig

Copy link
Copy Markdown
Contributor

Night-shift draft — not ready to merge; prepared for Zeno to review in the morning.

Linear: RND-11931 — "TOC feels unstable: groups expand/collapse or briefly lose context while navigating"

Proposed changes

Root cause. The sidebar table of contents is rendered inside the site layout (SpaceLayoutTableOfContents), i.e. the shared (content)/layout.tsx. In this app the layout's client subtree is remounted on client-side navigation — this is already documented in the codebase: useClearRouterCache.tsx keeps previousContextId in a module-level variable precisely because "the contextId gets reset on navigation … probably because of [Next.js #67542]" (a useRef there is wiped on every navigation, which only happens if the component remounts).

The two collapsible TOC components stored their expand/collapse state in component-local state, so that remount reset it on every navigation:

  • PageGroupItem.tsxuseState(true): a top-level group the visitor had collapsed snaps back open after navigating.
  • ToggleableLinkItem.tsxuseState(defaultIsOpen) + a useRef(hasBeenToggled) + an effect: nested groups re-derived from the freshly-mounting active path, the visitor's manual toggles were forgotten, and because the descendants used <AnimatePresence> with initial={hide}, the expand animation replayed on every mount — which reads as the sidebar flickering / losing its expanded context.

Fix. Move the groups' open/collapsed state into a small module-level store (useTOCGroupState, a zustand store keyed by page id) so it survives the navigation remount. This mirrors patterns already in the repo: the module-level workaround in useClearRouterCache and the zustand visitedPagesStore in useCurrentPage.tsx.

  • Until the visitor toggles a group, it follows the active page (unchanged behavior: auto-expand to reveal the current page). Once toggled, their choice is remembered for the session across navigations.
  • PageGroupItem and ToggleableLinkItem now read/write this store instead of local state; the now-redundant hasBeenToggled ref + sync effect are removed.
  • Descendants uses <AnimatePresence initial={false}> so an already-open group renders without replaying its expand animation on mount; visitor-initiated toggles still animate (they happen after mount).

No hydration risk: the store is empty on the server and on first client render, so the initial value equals the previous defaultIsOpen/true.

Changelog

  • [Fix] Table of contents no longer expands/collapses groups or loses its expanded context when navigating between pages.

For Zeno

  • Judgment calls:
    • Kept the fix scoped to state persistence + the mount-animation flag rather than trying to stop the layout remount itself (that's the underlying Next.js #67542 and would be a much larger, riskier change).
    • New behavior improvement: a visitor's manual expand/collapse now persists across navigations for the session (previously it was ephemeral because it was wiped on each remount). This felt like the intended UX and directly serves "stable", but flag if you'd rather keep toggles strictly ephemeral.
    • Store is keyed by page.id and lives for the SPA session (module-level, bounded by visited TOC items). Not persisted to storage — reloads start fresh, matching today's behavior.
  • Risks: Low. If the layout does not remount in some configs, the change is behavior-neutral-to-better (no regression). The AnimatePresence initial={false} change affects only the enter animation on mount.
  • Verified: bun run typecheck for gitbook (via turbo, 27 tasks green), bun run format, and biome check on the changed files — all clean.
  • Not verified: I did not do a live browser reproduction. The reported site (app.neosecurity.dev, VA enabled) needs auth I don't have, and reproducing the exact remount requires the dynamic/adaptive path. Root cause is inferred from the code + the existing in-repo useClearRouterCache workaround for the same "state reset on navigation" behavior. Suggested manual check: on a multi-section/VA site, collapse a TOC group, navigate to another page, and confirm the group stays collapsed and the sidebar doesn't flash.
  • Tests: No existing unit/Playwright tests cover these components, and per the repo's frontend testing guidance I didn't add a mocked React unit test. A Playwright test that navigates and asserts group aria-expanded/data-active persistence would be the right coverage but needs a fixture site whose layout actually remounts — worth a follow-up.

Generated by Claude Code

claude added 2 commits July 16, 2026 00:25
The sidebar table of contents lives in the site layout, whose client subtree is
remounted on client-side navigation (already documented in useClearRouterCache,
Next.js #67542). PageGroupItem and ToggleableLinkItem kept their expand/collapse
state in component-local useState/useRef, so every navigation reset it: top-level
groups snapped back open, nested groups re-derived from the freshly-mounting active
path, and the descendants' enter animation replayed each time - which read as the
sidebar flickering / losing its expanded context.

Move the groups' open/collapsed state into a module-level store keyed by page id so
it survives the navigation remount (mirroring the existing module-level workaround in
useClearRouterCache and the zustand store in useCurrentPage). Until the visitor
toggles a group it follows the active page; once toggled, their choice is remembered
for the session. Also set AnimatePresence initial={false} so an already-open group
does not replay its expand animation on mount.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaBLy53zu9DTce9sZhaARf
@linear-code

linear-code Bot commented Jul 16, 2026

Copy link
Copy Markdown

RND-11931

@changeset-bot

changeset-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b8e7ec1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
gitbook Patch

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

@argos-ci

argos-ci Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
customers-v2-cloudflare (Inspect) ⚠️ Changes detected (Review) 17 changed, 2 ignored Jul 16, 2026, 12:38 AM
customers-v2-vercel (Inspect) ⚠️ Changes detected (Review) 4 changed, 3 ignored Jul 16, 2026, 12:36 AM
v2-cloudflare (Inspect) ⚠️ Changes detected (Review) 76 changed, 2 ignored Jul 16, 2026, 12:39 AM
v2-vercel (Inspect) ⚠️ Changes detected (Review) 598 changed, 3 ignored Jul 16, 2026, 12:38 AM

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