Skip to content

fix(#675): stop MarkdownEditor re-render loop blocking worktree URL updates#677

Merged
Kewton merged 2 commits intodevelopfrom
feature/675-worktree
Apr 24, 2026
Merged

fix(#675): stop MarkdownEditor re-render loop blocking worktree URL updates#677
Kewton merged 2 commits intodevelopfrom
feature/675-worktree

Conversation

@Kewton
Copy link
Copy Markdown
Owner

@Kewton Kewton commented Apr 24, 2026

Summary

.md ファイル表示中にサイドバーから別ブランチを選択しても URL が更新されず画面遷移しないバグを修正。MarkdownEditor 経由で発生していた無限 re-render ループが router.push の App Router transition を starve させていた根本原因を A案 + B案 の多層防御で解消。

Closes #675

Root Cause

useFileTabs の return が毎レンダー新オブジェクト参照を返すため、WorktreeDetailRefactored の 4 つの useCallback (deps=[fileTabs]) が毎レンダー再生成される。これが FilePanelContentMarkdownEditor へ不安定 prop として伝播し、useEffect([isDirty, onDirtyChange]) が毎レンダー発火 → dispatch({type:'SET_DIRTY'}) → reducer が同値でも新 state を返す → 上流再レンダー → ループ。高優先度 dispatch が積まれ続けるため router.push が起動した低優先度 transition が starve し URL が commit されない。.md 以外で再現しないのは MarkdownEditor だけが onDirtyChange useEffect を持つため。

Changes

Fixed

  • A案: src/components/worktree/WorktreeDetailRefactored.tsxhandleLoadContent / handleLoadError / handleSetLoading / handleDirtyChangeuseCallback deps を [fileTabs] から [fileTabs.dispatch] に変更。dispatchuseReducer 由来で identity stable なためコールバック identity が安定し、下流への不安定 prop 伝播が止まる。
  • B案: src/hooks/useFileTabs.tsSET_DIRTY reducer で対象 tab の isDirty が既に同値なら state を同一参照のまま返す no-op 判定を追加。useReducer は state 参照が同一なら再レンダーをスキップするため、将来別経路で deps が崩れても SET_DIRTY 経由のループは構造的に防がれる多層防御。

Added

  • tests/unit/hooks/useFileTabs.test.ts: SET_DIRTY 同値 no-op 検証 3 件 (false→false / true→true / double-apply による state 参照安定性)。

Out of Scope

同ファイル (WorktreeDetailRefactored.tsx) 内の他 5 箇所 (L551/L570/L581/L875/L904) にも『fileTabs 全体を deps に置く』同種アンチパターンが残るが、Issue #675 の再現経路とは別系統のため別 Issue として起票予定。

Test Results

Unit Tests

npm run test:unit
Test Files  340 passed (340)
Tests       6384 passed | 7 skipped (6391)
Duration    14.27s

Lint & Type Check

  • ESLint: 0 errors / 0 warnings
  • TypeScript: 0 errors (tsc --noEmit exit 0)

Build

npm run build
Compiled successfully

Manual Verification (recommended before merge)

  • Worktree A で .md を開いた状態でサイドバーから Worktree B を選択 → URL が /worktrees/B に変わる
  • .yaml / .yml でも同様に遷移する (MarkdownEditor を経由する全拡張子で同じ構造のため)
  • .png / .json / .pdf で回帰なし (元々再現しないので挙動維持のはず)

Checklist

  • Unit tests pass
  • Lint check passes
  • Type check passes
  • Build succeeds
  • No console.log in production code
  • Scope kept minimal (no unrelated refactoring)

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

Kewton and others added 2 commits April 24, 2026 20:30
- useFileTabs: SET_DIRTY short-circuits when target tab isDirty is unchanged,
  so upstream useReducer skips re-render and MarkdownEditor's onDirtyChange
  useEffect no longer feeds a loop.
- WorktreeDetailRefactored: useCallback deps for file-panel handlers switched
  from [fileTabs] to [fileTabs.dispatch] to stabilise callback identity and
  stop MarkdownEditor from re-firing onDirtyChange every render. This unblocks
  router.push transitions when switching branches from the sidebar while a
  .md file is open.
- Adds reducer tests covering same-value SET_DIRTY no-op (false→false, true→true,
  double-apply stable reference).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… acceptance, progress)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Kewton Kewton added the bug Something isn't working label Apr 24, 2026
@Kewton Kewton merged commit c621e37 into develop Apr 24, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant