Skip to content

Refactor: Decompose useLayoutEventListeners god hook #50

@dannysmith

Description

@dannysmith

Overview

Decompose the monolithic useLayoutEventListeners hook (486 lines) into focused, single-responsibility hooks to improve maintainability, testability, and reduce coupling.

Priority: Post-1.0.0 tech debt
Hook: src/hooks/useLayoutEventListeners.ts (486 lines)
Type: Code organization / maintainability issue (not a bug)
Impact: High maintenance burden, medium performance impact

Current Responsibilities

The hook currently manages 8 distinct concerns:

  1. Keyboard Shortcuts (7 shortcuts via useHotkeys):

    • mod+s, mod+1, mod+2, mod+n, mod+w, mod+comma, mod+0
  2. DOM Custom Events (11+ listeners):

    • open-preferences, create-new-file, toggle-focus-mode, toggle-typewriter-mode
    • Parts-of-speech highlighting toggles (5 events)
    • toggle-all-highlights, file-opened
  3. Tauri Menu Events (15 listeners):

    • Project, file operations, formatting commands
  4. Editor Focus State Management:

    • Tracks window.isEditorFocused
    • Updates format menu enabled/disabled state
  5. Project Initialization:

    • Loads persisted project on mount
  6. Rust Toast Bridge:

    • Initializes bi-directional toast communication
  7. Preferences Dialog State:

    • Manages dialog open/closed state
  8. Parts of Speech Highlighting:

    • Complex settings updates for copyedit mode

Problems

1. Single Responsibility Principle Violation

One hook manages 8+ unrelated concerns, making it difficult to understand.

2. Hard to Test

Cannot test keyboard shortcuts in isolation from menu events or DOM events.

3. High Coupling

Changes to one concern require touching code adjacent to completely unrelated concerns.

4. Performance Impact

Giant hook re-runs whenever any dependency changes.

5. Hard to Maintain

487 lines of deeply nested useEffect calls with complex cleanup logic.

6. Code Duplication

  • Menu format listeners have nearly identical structure
  • Hotkey options duplicated across 5 shortcuts
  • Parts-of-speech highlight toggles create 5 wrapper functions calling same base

Recommended Approach

Decompose into focused hooks:

// hooks/useKeyboardShortcuts.ts
export function useKeyboardShortcuts() {
  const defaultOpts = {
    preventDefault: true,
    enableOnFormTags: ['input', 'textarea', 'select'],
    enableOnContentEditable: true
  }
  
  useHotkeys('mod+s', () => {...}, { preventDefault: true })
  useHotkeys('mod+1', () => {...}, defaultOpts)
  // ... etc
}

// hooks/useMenuEvents.ts
export function useMenuEvents() {
  // All Tauri menu listeners
  // Map-based approach for format menu events
}

// hooks/useDOMEventListeners.ts
export function useDOMEventListeners() {
  // All window.addEventListener calls
  // Or split further into useHighlightEvents, useFileEvents
}

// hooks/useEditorFocusTracking.ts
export function useEditorFocusTracking() {
  // Format menu state management
}

// hooks/useProjectInitialization.ts
export function useProjectInitialization() {
  // Project loading on mount
}

// hooks/useRustToastBridge.ts
export function useRustToastBridge() {
  // Toast bridge initialization
}

// Layout.tsx - compose the hooks
function Layout() {
  useKeyboardShortcuts()
  useMenuEvents()
  useDOMEventListeners()
  useEditorFocusTracking()
  useProjectInitialization()
  useRustToastBridge()
  
  const [preferencesOpen, setPreferencesOpen] = useState(false)
  
  // ... rest of layout
}

Benefits

  1. Testability: Each hook can be tested in isolation
  2. Clear Dependencies: Dependencies scoped to each concern
  3. Maintainability: Easy to find and modify specific functionality
  4. Feature Toggles: Can disable/enable features by commenting out hooks
  5. Performance: Smaller hooks re-run less frequently
  6. Reduced Duplication: Easier to extract common patterns in focused contexts

Duplication to Address

While decomposing, address these specific issues:

  1. Menu format event map:

    const formatEventMap = {
      'menu-format-bold': 'toggleBold',
      'menu-format-italic': 'toggleItalic',
      'menu-format-link': 'createLink',
      'menu-format-h1': ['formatHeading', 1],
      // ... etc
    }
  2. Shared hotkey options: Extract common options object

  3. Parts-of-speech wrapper functions: Generate from array instead of 5 separate functions

  4. Open project flow: Extract to lib/projects/actions.ts

Related Work

This task is related to the Event Bridge Refactor. The Event Bridge refactor proposes replacing DOM custom events with a callback registry pattern.

Recommendation: These could be tackled together or in sequence. If Event Bridge happens first, this decomposition will be easier. If this happens first, it will make the Event Bridge refactor easier.

Success Criteria

  • Hook is decomposed into 5-7 focused hooks
  • Each hook has a single, clear responsibility
  • Layout.tsx composes the hooks cleanly
  • All functionality still works (no regressions)
  • Duplication issues are addressed
  • Each new hook could be tested in isolation
  • Code is easier to navigate and understand

Source Reviews

  • docs/reviews/2025-staff-engineering-review.md:345-420
  • docs/reviews/code-review-2025-10-24.md:15-28
  • docs/reviews/2025-10-24-duplication-review.md:48-51, 106-108, 123-126
  • docs/reviews/analyysis-of-reviews.md:113-119

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions