Skip to content

fix(hierarchical-document-list): keep the tree mounted while an Activity boundary hides it - #2181

Draft
stipsan wants to merge 1 commit into
mainfrom
cody/hierarchical-document-list-keep-tree-while-hidden-8079
Draft

stipsan wants to merge 1 commit into
mainfrom
cody/hierarchical-document-list-keep-tree-while-hidden-8079

Conversation

@stipsan

@stipsan stipsan commented Oct 13, 2026

Copy link
Copy Markdown
Member

Description

With beta.reactActivityMode, hiding the Structure tool with a hierarchy open unmounted the whole tree in the background and the reveal rebuilt it. The Studio keeps recently used tools mounted behind <Activity>, which detaches refs while a tool is hidden (callback refs are called with null) and attaches them again on reveal. TreeEditor kept the element its DndProvider needs in state through ref={setContext} and only rendered the tree while that state was set, so the detach dropped the drag-and-drop provider, every row and every document in the "Add more items" list, and the reveal mounted them all again (the same pattern as sanity-io/sanity#15309's useElementState).

The box is always rendered, so the only null React ever passes is that detach; the ref callback now ignores it and the element lives as long as the editor.

What to review

  • TreeEditor.tsx: the ref callback on the Box.
  • package.json / pnpm-lock.yaml: @testing-library/react and jsdom as catalog: devDependencies for the test (the lockfile change is the two importer entries).

Affects @sanity/hierarchical-document-list.

Testing

  • TreeEditor.test.tsx (jsdom, Studio hooks mocked): the box's child, the tree, is the same element while an <Activity> boundary hides the editor and after it shows it again. On main the box is empty while hidden.
  • pnpm --filter @sanity/hierarchical-document-list exec vitest run, pnpm build, pnpm lint, pnpm knip, pnpm format are clean.
  • Measured in dev/test-studio (Structure → Document list builders → Main table of contents, reactActivityMode on) with a headed Playwright harness that switches to Vision, waits 7s, switches back and records React commits and mounted components until the page is quiet for 30 frames. Before: 8731 components mounted on reveal, starting with DndProvider and 38 DocumentInNodes (3302 in one commit). After: 5005, none of them the tree itself. What remains is not this plugin's: @nosferatu500/react-sortable-tree remounts its rows on every re-render (its mergedProps memo is keyed on props), and the reveal re-renders it three times (the edit state and the preview store reconnecting, the useAllItems refetch), plus the @sanity/ui tooltip contents of the re-resolved document previews. The tool area paints in 1 frame before and after; the background unmount was invisible, only wasteful, and it reset the tree's internal state (open menus, the list's scroll position) on every switch.

Notes for release

Fixes the hierarchy tree being torn down and rebuilt on every switch away from and back to the Structure tool in a Studio with beta.reactActivityMode enabled.

Open in Web Open in Cursor 

@vercel

vercel Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
plugins-studio Ready Ready Preview Oct 27, 2026 12:06pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
plugins-e2e-test-studio Ignored Ignored Oct 27, 2026 12:06pm UTC

Request Review

@changeset-bot

changeset-bot Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: baa8bcd

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

This PR includes changesets to release 1 package
Name Type
@sanity/hierarchical-document-list 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

@stipsan stipsan added the ✨ ai PR authored by an AI agent (🤖 bot is for CI automation/Renovate) label Oct 13, 2026 — with Cursor
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

✅ E2E Tests

🟢 46 passed • view full report • view run

Studio: https://plugins-e2e-test-studio-3ue08fyx9.sanity.dev

Datasets: pr-2181-chromium-40500855181, pr-2181-firefox-40500855181

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 38.77% 7809 / 20138
🔵 Statements 38.72% 8398 / 21686
🔵 Functions 34.51% 2140 / 6201
🔵 Branches 30.84% 4522 / 14662
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
plugins/@sanity/hierarchical-document-list/src/components/TreeEditor.tsx 81.25% 78.94% 57.14% 82.75% 73, 141-193, 200
Generated in workflow #11400 for commit baa8bcd by the Vitest Coverage Report Action

@cursor

cursor Bot commented Oct 13, 2026

Copy link
Copy Markdown

Review: approve

Verified

  • TreeEditor.test.tsx passes on this branch. With main's TreeEditor.tsx under the same test it fails with expected null to be <div …> (the box is empty while hidden), so the test fails without the fix.
  • The premise holds: the Box is unconditional and nothing else writes context, so the only null the callback ever sees is the detach. The callback returns nothing, so React 19 still calls it with null rather than a cleanup, and the guard is what keeps the element.
  • CI green, lockfile diff is the two importer entries, no public API change, changeset is a patch with a consumer-facing summary.

Notes (none blocking)

  • The before/after component counts come from a harness that is not in the repo, so I could not reproduce them. The mechanism is pinned by the test, and attributing the remaining remounts to @nosferatu500/react-sortable-tree matches its mergedProps memo being keyed on props.
  • fix(core): keep element state while an Activity boundary hides the tool sanity#15309 (useElementState) is still open and @internal, so an inline callback is the right call here; if that lands as public API this becomes a one-line swap.

Verdict

Approve. Small, correct, and the test guards the regression.

@cursor
cursor Bot force-pushed the cody/hierarchical-document-list-keep-tree-while-hidden-8079 branch from 310bd1c to cf74bf4 Compare October 22, 2026 16:29
@cursor
cursor Bot force-pushed the cody/hierarchical-document-list-keep-tree-while-hidden-8079 branch from cf74bf4 to 4b18c43 Compare October 23, 2026 13:30
@cursor
cursor Bot force-pushed the cody/hierarchical-document-list-keep-tree-while-hidden-8079 branch from 4b18c43 to f6e6627 Compare October 26, 2026 16:07
…ity boundary hides it

With `beta.reactActivityMode` the Studio keeps recently used tools mounted behind
`<Activity>`, which detaches refs while a tool is hidden and attaches them again on
reveal. `TreeEditor` kept the element its `DndProvider` needs in state through
`ref={setContext}` and rendered the tree only while that state was set, so the detach
unmounted the whole tree (the drag-and-drop provider, every row and every document in
the "Add more items" list) in the background and the reveal rebuilt it.

The box is always rendered, so the only `null` React ever passes is that detach; the
ref callback now ignores it and the element lives as long as the editor.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
@cursor
cursor Bot force-pushed the cody/hierarchical-document-list-keep-tree-while-hidden-8079 branch from f6e6627 to baa8bcd Compare October 27, 2026 12:03

This branch was successfully deployed

1 active deployment
Preview – plugins-studio — baa8bcdc Deployed Oct 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ ai PR authored by an AI agent (🤖 bot is for CI automation/Renovate)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants