Skip to content

fix(trees): persist FileTreeView initial-snapshot flag across re-subscribes#918

Merged
SlexAxton merged 1 commit into
mainfrom
fix/issue-883-filetree-resubscribe
Jun 30, 2026
Merged

fix(trees): persist FileTreeView initial-snapshot flag across re-subscribes#918
SlexAxton merged 1 commit into
mainfrom
fix/issue-883-filetree-resubscribe

Conversation

@claude

@claude claude Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Requested by Alex · Slack thread

Description

FileTreeView's controller-subscription effect re-subscribes whenever its layout/viewport deps change (controller, initialViewportHeight, itemHeight, overscan, stickyFolders). The guard that suppresses the redundant re-render on the genuine initial snapshot lived in an effect-local let hasSeenInitialControllerSnapshot = false;, so it reset to false on every re-subscribe — not just on mount.

This change hoists the flag to a component-instance useRef so the suppression happens only once per component instance, and extracts the bump decision into a small pure helper, shouldBumpControllerRevision, following the existing rowClickPlan / focusHelpers convention.

Before: A host that keeps the tree mounted and applies live controller mutations could intermittently drop a mutation — whenever a real mutation landed as the first emit after a re-subscribe, the model updated (and the imperative update() ran) but the render revision was never bumped, so the rendered rows stayed stale until the next emit.

After: Every re-subscribe still skips only the genuine initial snapshot; the first real emit after any re-subscribe bumps the revision and repaints. Hosts that fully remount on each change were never affected (the remount masked it), so no behavior change for them.

Motivation & Context

Fixes the stale-DOM-after-re-subscribe bug reported in #883, where the model was correct but the rendered rows lagged a mutation behind for mounted, live-updating trees.

How

  • packages/trees/src/render/FileTreeView.tsx: replace the effect-local let with a component-level hasSeenInitialControllerSnapshotRef = useRef(false), and drive the subscribe callback through the new helper.
  • packages/trees/src/render/controllerSnapshotSubscription.ts (new): pure shouldBumpControllerRevision(holder) — suppresses only the first emit observed by a given persistent holder, bumps every emit after that.

Type of changes

  • Bug fix (non-breaking change which fixes an issue)
  • Refactoring (non-breaking change)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would change existing functionality)
  • Documentation update

Tests

Added packages/trees/test/controller-snapshot-subscription.test.ts (bun:test) covering the re-subscribe-then-first-emit path:

  • only the genuine initial snapshot is suppressed; every later emit bumps;
  • the first emit after a re-subscribe still bumps when the holder persists (the fix);
  • a regression guard that reproduces the old behavior — a fresh holder per re-subscribe swallows the first emit — documenting exactly why the effect-local let was wrong.

Full packages/trees suite passes locally (307 tests). oxlint, oxfmt --check, and tsgo --noEmit are clean on the changed files.

Note on coverage: the literal stale-DOM symptom is currently masked in a synchronous jsdom render because the subscribe callback's update() calls setLayoutState with a fresh object every emit, which itself forces a re-render. The added tests therefore pin the suppression-flag contract directly (the exact behavior the fix changes) rather than asserting DOM staleness that the test harness cannot reproduce.

Checklist

  • I have read the contributing guidelines
  • My code follows the code style of the project
  • My code is formatted properly (oxfmt --check, oxlint)
  • I have updated the documentation accordingly (if applicable)
  • I have added tests to cover my changes
  • All new and existing tests pass (packages/trees bun:test)

How was AI used in generating this PR

Authored with Claude Code (Claude Opus 4.8): diagnosis, the useRef fix, the extracted pure helper, and the regression tests. A human should review before merge.

Related issues

Fixes #883


Generated by Claude Code

…cribes

The controller-subscription effect re-subscribes whenever its layout/viewport
deps change. The "have we seen the initial snapshot yet?" guard lived in an
effect-local `let`, so it reset to false on every re-subscribe and swallowed
the render-revision bump on the first real emit after each re-subscribe — the
model updated but the rendered rows went stale until the next emit.

Hoist the flag to a component-instance `useRef` so the initial-render
optimization is preserved while every re-subscribe still repaints on its first
real emit. The bump decision is extracted into a pure, unit-testable helper
(shouldBumpControllerRevision) following the existing rowClickPlan/focusHelpers
convention.

Fixes #883

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDXG4jRZT8sPS1YBSes2pR
@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
pierre-docs-diffs Ready Ready Preview Jun 30, 2026 4:56pm
pierre-docs-diffshub Ready Ready Preview Jun 30, 2026 4:56pm
pierre-docs-trees Ready Ready Preview Jun 30, 2026 4:56pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
pierrejs-diff-demo Skipped Skipped Jun 30, 2026 4:56pm

Request Review

@SlexAxton SlexAxton marked this pull request as ready for review June 30, 2026 21:24
@SlexAxton SlexAxton merged commit 37e7ef0 into main Jun 30, 2026
8 checks passed
@SlexAxton SlexAxton deleted the fix/issue-883-filetree-resubscribe branch June 30, 2026 21:31
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.

FileTreeView swallows the first controller emit after every re-subscribe (live mutation updates model, not DOM)

2 participants