Files
OpenNoodl/dev-docs/tasks/phase-4-canvas-visualisation-views/PREREQ-002-react19-debug-fixes/CHANGELOG.md
2026-01-04 00:17:33 +01:00

8.0 KiB

PREREQ-002: React 19 Debug Fixes - CHANGELOG

Status: COMPLETED

Completion Date: March 1, 2026


Overview

Fixed React 18/19 createRoot memory leaks and performance issues where new React roots were being created unnecessarily instead of reusing existing roots. These issues caused memory accumulation and potential performance degradation over time.


Problem Statement

Issue 1: ConnectionPopup Memory Leaks

In nodegrapheditor.ts, the openConnectionPanels() method created React roots properly for the initial render, but then created new roots inside the onPortSelected callback instead of reusing the existing roots. This caused a new React root to be created every time a user selected connection ports.

Issue 2: Hot Module Replacement Root Duplication

In router.tsx, the HMR (Hot Module Replacement) accept handlers created new React roots on every hot reload instead of reusing the existing roots stored in variables.

Issue 3: News Modal Root Accumulation

In whats-new.ts, a new React root was created each time the modal opened without properly unmounting and cleaning up the previous root when the modal closed.


Changes Made

1. Fixed ConnectionPopup Root Leaks

File: packages/noodl-editor/src/editor/src/views/nodegrapheditor.ts

Problem Pattern:

// BROKEN - Created new roots in callbacks
const fromDiv = document.createElement('div');
const root = createRoot(fromDiv);  // Created once
root.render(...);

onPortSelected: (fromPort) => {
  createRoot(toDiv).render(...);    // ❌ NEW root every selection!
  createRoot(fromDiv).render(...);  // ❌ NEW root every selection!
}

Fixed Pattern:

// FIXED - Reuses cached roots
const fromDiv = document.createElement('div');
const fromRoot = createRoot(fromDiv);  // Created once
fromRoot.render(...);

const toDiv = document.createElement('div');
const toRoot = createRoot(toDiv);     // Created once
toRoot.render(...);

onPortSelected: (fromPort) => {
  toRoot.render(...);    // ✅ Reuses root
  fromRoot.render(...);  // ✅ Reuses root
}

onClose: () => {
  fromRoot.unmount();    // ✅ Proper cleanup
  toRoot.unmount();      // ✅ Proper cleanup
}

Impact:

  • Prevents memory leak on every connection port selection
  • Improves performance when creating multiple node connections
  • Proper cleanup when connection panels close

2. Fixed HMR Root Duplication

File: packages/noodl-editor/src/editor/src/router.tsx

Problem Pattern:

// BROKEN - Created new root on every HMR
function createToastLayer() {
  const toastLayer = document.createElement('div');
  createRoot(toastLayer).render(...);

  if (import.meta.webpackHot) {
    import.meta.webpackHot.accept('./views/ToastLayer', () => {
      createRoot(toastLayer).render(...);  // ❌ NEW root on HMR!
    });
  }
}

Fixed Pattern:

// FIXED - Stores and reuses roots
let toastLayerRoot: ReturnType<typeof createRoot> | null = null;
let dialogLayerRoot: ReturnType<typeof createRoot> | null = null;

function createToastLayer() {
  const toastLayer = document.createElement('div');
  toastLayerRoot = createRoot(toastLayer);
  toastLayerRoot.render(...);

  if (import.meta.webpackHot) {
    import.meta.webpackHot.accept('./views/ToastLayer', () => {
      if (toastLayerRoot) {
        toastLayerRoot.render(...);  // ✅ Reuses root!
      }
    });
  }
}

Impact:

  • Prevents root accumulation during development HMR
  • Improves hot reload performance
  • Reduces memory usage during development

3. Fixed News Modal Root Accumulation

File: packages/noodl-editor/src/editor/src/whats-new.ts

Problem Pattern:

// BROKEN - No cleanup when modal closes
createRoot(modalContainer).render(
  React.createElement(NewsModal, {
    content: latestChangelogPost.content_html,
    onFinished: () => ipcRenderer.send('viewer-show') // ❌ No cleanup!
  })
);

Fixed Pattern:

// FIXED - Properly unmounts root and removes DOM
const modalRoot = createRoot(modalContainer);
modalRoot.render(
  React.createElement(NewsModal, {
    content: latestChangelogPost.content_html,
    onFinished: () => {
      ipcRenderer.send('viewer-show');
      modalRoot.unmount(); // ✅ Unmount root
      modalContainer.remove(); // ✅ Remove DOM
    }
  })
);

Impact:

  • Prevents root accumulation when changelog modal is shown multiple times
  • Proper DOM cleanup
  • Better memory management

React Root Lifecycle Best Practices

Correct Pattern: Create Once, Reuse, Unmount

// 1. Create root ONCE
const container = document.createElement('div');
const root = createRoot(container);

// 2. REUSE root for updates
root.render(<MyComponent prop="value1" />);
root.render(<MyComponent prop="value2" />); // Same root!

// 3. UNMOUNT when done
root.unmount();
container.remove(); // Optional: cleanup DOM

Anti-Pattern: Creating New Roots

// DON'T create new roots for updates
createRoot(container).render(<MyComponent prop="value1" />);
createRoot(container).render(<MyComponent prop="value2" />); // ❌ Memory leak!

Pattern for Conditional/Instance Roots

// Store root as instance variable
class MyView {
  private root: ReturnType<typeof createRoot> | null = null;

  render() {
    if (!this.root) {
      this.root = createRoot(this.el);
    }
    this.root.render(<MyComponent />);
  }

  dispose() {
    if (this.root) {
      this.root.unmount();
      this.root = null;
    }
  }
}

Verification

Audit Results

Searched entire codebase for createRoot usage patterns. Found 36 instances across 26 files. Analysis:

Already Correct (23 files):

  • Most files already use the if (!this.root) pattern correctly
  • Store roots as instance/class variables
  • Properly gate root creation

Fixed (3 files):

  1. nodegrapheditor.ts - Connection popup root reuse
  2. router.tsx - HMR root caching
  3. whats-new.ts - Modal cleanup

No Issues Found:

  • No other problematic patterns detected
  • All other usages follow React 18/19 best practices

Test Verification

To verify these fixes:

  1. Test ConnectionPopup:

    • Create multiple node connections
    • Select different ports repeatedly
    • Memory should remain stable
  2. Test HMR:

    • Make changes to ToastLayer/DialogLayer components
    • Hot reload multiple times
    • Dev tools should show stable root count
  3. Test News Modal:

    • Trigger changelog modal multiple times (adjust localStorage dates)
    • Memory should not accumulate

Files Modified

packages/noodl-editor/src/editor/src/
├── views/
│   ├── nodegrapheditor.ts          # ConnectionPopup root lifecycle
│   └── whats-new.ts                # News modal cleanup
└── router.tsx                       # HMR root caching


Follow-up Actions

Completed

  • Fix nodegrapheditor.ts ConnectionPopup leaks
  • Fix router.tsx HMR root duplication
  • Fix whats-new.ts modal cleanup
  • Audit all createRoot usage in codebase
  • Document best practices

Future Considerations 💡

  • Consider adding ESLint rule to catch createRoot anti-patterns
  • Add memory profiling tests to CI for regression detection
  • Create developer guide section on React root management

Notes

  • Breaking Change: None - all changes are internal improvements
  • Performance Impact: Positive - reduces memory usage
  • Development Impact: Better HMR experience with no root accumulation

Key Learning: In React 18/19, createRoot returns a root object that should be reused for subsequent renders to the same DOM container. Creating new roots for the same container causes memory leaks and degrades performance.