Files
OpenNoodl/dev-docs/future-projects/PHASE-RUNTIME-REACT-19-MIGRATION.md

27 KiB
Raw Blame History

Phase: Runtime React 19 Migration

Overview

This phase modernizes the OpenNoodl runtime (the code that powers deployed/published projects) from React 17 to React 19. Unlike the editor migration, this directly affects end-user applications in production.

Key Principle: No one gets left behind. Users choose when to migrate, with comprehensive tooling to guide them.

Goals

  1. Dual Runtime Support - Allow users to deploy to either React 17 (legacy) or React 19 (modern) runtime
  2. Migration Detection System - Automatically scan projects for React 19 incompatibilities
  3. Guided Migration - Provide clear, actionable guidance for fixing compatibility issues
  4. Zero Breaking Changes for Passive Users - Projects that don't explicitly opt-in continue working unchanged

Architecture

Dual Runtime System

┌─────────────────────────────────────────────────────────────┐
│                     OpenNoodl Editor                         │
├─────────────────────────────────────────────────────────────┤
│                    Deploy/Publish Dialog                     │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  Runtime Version: [React 17 (Legacy) ▼]             │    │
│  │                   [React 19 (Modern)  ]             │    │
│  │                                                     │    │
│  │  ⚠️ Migration Status: 2 issues detected            │    │
│  │     [Run Migration Check] [View Details]           │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘
                              │
              ┌───────────────┴───────────────┐
              ▼                               ▼
┌─────────────────────────┐     ┌─────────────────────────┐
│   noodl-viewer-react    │     │   noodl-viewer-react    │
│      (React 17)         │     │      (React 19)         │
│                         │     │                         │
│  • Legacy lifecycle     │     │  • Modern lifecycle     │
│  • ReactDOM.render()    │     │  • createRoot()         │
│  • String refs support  │     │  • Strict mode ready    │
└─────────────────────────┘     └─────────────────────────┘

Package Structure

packages/
├── noodl-viewer-react/
│   ├── src/
│   │   ├── index.js              # Shared entry logic
│   │   ├── init-legacy.js        # React 17 initialization
│   │   └── init-modern.js        # React 19 initialization
│   ├── static/
│   │   ├── deploy/               # React 17 bundle (default)
│   │   └── deploy-react19/       # React 19 bundle
│   └── webpack-configs/
│       ├── webpack.deploy.legacy.js
│       └── webpack.deploy.modern.js
├── noodl-viewer-cloud/
│   └── [similar structure]
└── noodl-runtime/
    └── src/
        ├── compat/
        │   ├── react17-shims.js  # Compatibility layer
        │   └── react19-shims.js
        └── migration/
            ├── detector.js       # Incompatibility detection
            └── reporter.js       # Migration report generation

Migration Detection System

Detected Patterns

The migration system scans for the following incompatibilities:

Critical (Will Break)

Pattern Detection Method Migration Path
componentWillMount AST scan of JS nodes Convert to constructor or componentDidMount
componentWillReceiveProps AST scan of JS nodes Convert to static getDerivedStateFromProps or componentDidUpdate
componentWillUpdate AST scan of JS nodes Convert to getSnapshotBeforeUpdate + componentDidUpdate
ReactDOM.render() String match in custom code Convert to createRoot().render()
String refs (ref="myRef") Regex in JSX Convert to React.createRef() or callback refs
contextTypes / getChildContext AST scan Convert to React.createContext
createFactory() String match Convert to JSX or createElement

Warning (Deprecated but Functional)

Pattern Detection Method Recommendation
defaultProps on function components AST scan Use ES6 default parameters
propTypes Import detection Consider TypeScript or remove
findDOMNode() String match Use refs instead

Info (Best Practice)

Pattern Detection Method Recommendation
Class components AST scan Consider converting to functional + hooks
UNSAFE_ lifecycle methods String match Plan migration to modern patterns

Detection Implementation

// packages/noodl-runtime/src/migration/detector.js

const CRITICAL_PATTERNS = [
  {
    id: 'componentWillMount',
    pattern: /componentWillMount\s*\(/,
    severity: 'critical',
    title: 'componentWillMount is removed in React 19',
    description: 'This lifecycle method has been removed. Move initialization logic to the constructor or componentDidMount.',
    autoFixable: false,
    documentation: 'https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-deprecated-react-apis',
    migration: {
      before: `componentWillMount() {\n  this.setState({ data: fetchData() });\n}`,
      after: `componentDidMount() {\n  this.setState({ data: fetchData() });\n}`
    }
  },
  {
    id: 'componentWillReceiveProps',
    pattern: /componentWillReceiveProps\s*\(/,
    severity: 'critical',
    title: 'componentWillReceiveProps is removed in React 19',
    description: 'Use static getDerivedStateFromProps or componentDidUpdate instead.',
    autoFixable: false,
    documentation: 'https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-deprecated-react-apis',
    migration: {
      before: `componentWillReceiveProps(nextProps) {\n  if (nextProps.id !== this.props.id) {\n    this.setState({ data: null });\n  }\n}`,
      after: `static getDerivedStateFromProps(props, state) {\n  if (props.id !== state.prevId) {\n    return { data: null, prevId: props.id };\n  }\n  return null;\n}`
    }
  },
  {
    id: 'componentWillUpdate',
    pattern: /componentWillUpdate\s*\(/,
    severity: 'critical',
    title: 'componentWillUpdate is removed in React 19',
    description: 'Use getSnapshotBeforeUpdate with componentDidUpdate instead.',
    autoFixable: false,
    documentation: 'https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-deprecated-react-apis'
  },
  {
    id: 'reactdom-render',
    pattern: /ReactDOM\.render\s*\(/,
    severity: 'critical',
    title: 'ReactDOM.render is removed in React 19',
    description: 'Use createRoot from react-dom/client instead.',
    autoFixable: true,
    migration: {
      before: `import { render } from 'react-dom';\nrender(<App />, document.getElementById('root'));`,
      after: `import { createRoot } from 'react-dom/client';\nconst root = createRoot(document.getElementById('root'));\nroot.render(<App />);`
    }
  },
  {
    id: 'string-refs',
    pattern: /ref\s*=\s*["'][^"']+["']/,
    severity: 'critical',
    title: 'String refs are removed in React 19',
    description: 'Use React.createRef() or callback refs instead.',
    autoFixable: false,
    migration: {
      before: `<input ref="myInput" />`,
      after: `// Using createRef:\nmyInputRef = React.createRef();\n<input ref={this.myInputRef} />\n\n// Using callback ref:\n<input ref={el => this.myInput = el} />`
    }
  },
  {
    id: 'legacy-context',
    pattern: /contextTypes\s*=|getChildContext\s*\(/,
    severity: 'critical',
    title: 'Legacy Context API is removed in React 19',
    description: 'Migrate to React.createContext and useContext.',
    autoFixable: false,
    documentation: 'https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-legacy-context'
  }
];

const WARNING_PATTERNS = [
  {
    id: 'defaultProps-function',
    pattern: /\.defaultProps\s*=/,
    severity: 'warning',
    title: 'defaultProps on function components is deprecated',
    description: 'Use ES6 default parameters instead. Class components still support defaultProps.',
    autoFixable: true
  },
  {
    id: 'propTypes',
    pattern: /\.propTypes\s*=|from\s*['"]prop-types['"]/,
    severity: 'warning',
    title: 'PropTypes are removed from React',
    description: 'Consider using TypeScript for type checking, or remove propTypes.',
    autoFixable: false
  }
];

class MigrationDetector {
  constructor() {
    this.patterns = [...CRITICAL_PATTERNS, ...WARNING_PATTERNS];
  }

  scanNode(nodeData) {
    const issues = [];
    const code = this.extractCode(nodeData);
    
    if (!code) return issues;

    for (const pattern of this.patterns) {
      if (pattern.pattern.test(code)) {
        issues.push({
          ...pattern,
          nodeId: nodeData.id,
          nodeName: nodeData.name || nodeData.type,
          location: this.findLocation(code, pattern.pattern)
        });
      }
    }

    return issues;
  }

  scanProject(projectData) {
    const report = {
      timestamp: new Date().toISOString(),
      projectName: projectData.name,
      summary: {
        critical: 0,
        warning: 0,
        info: 0,
        canMigrate: true
      },
      issues: [],
      affectedNodes: new Set()
    };

    // Scan all components
    for (const component of projectData.components || []) {
      for (const node of component.nodes || []) {
        const nodeIssues = this.scanNode(node);
        
        for (const issue of nodeIssues) {
          report.issues.push({
            ...issue,
            component: component.name
          });
          report.summary[issue.severity]++;
          report.affectedNodes.add(node.id);
        }
      }
    }

    // Check custom modules
    for (const module of projectData.modules || []) {
      const moduleIssues = this.scanCustomModule(module);
      report.issues.push(...moduleIssues);
    }

    report.summary.canMigrate = report.summary.critical === 0;
    report.affectedNodes = Array.from(report.affectedNodes);

    return report;
  }

  extractCode(nodeData) {
    // Extract JavaScript code from various node types
    if (nodeData.type === 'JavaScriptFunction' || nodeData.type === 'Javascript2') {
      return nodeData.parameters?.code || nodeData.parameters?.Script || '';
    }
    if (nodeData.type === 'Expression') {
      return nodeData.parameters?.expression || '';
    }
    // Custom React component nodes
    if (nodeData.parameters?.reactComponent) {
      return nodeData.parameters.reactComponent;
    }
    return '';
  }

  findLocation(code, pattern) {
    const match = code.match(pattern);
    if (!match) return null;
    
    const lines = code.substring(0, match.index).split('\n');
    return {
      line: lines.length,
      column: lines[lines.length - 1].length
    };
  }
}

module.exports = { MigrationDetector, CRITICAL_PATTERNS, WARNING_PATTERNS };

User Interface

Deploy Dialog Enhancement

┌──────────────────────────────────────────────────────────────────┐
│                        Deploy Project                             │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Target: [Production Server ▼]                                   │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │  Runtime Version                                           │  │
│  │                                                            │  │
│  │  ○ React 17 (Legacy)                                       │  │
│  │    Stable, compatible with all existing code               │  │
│  │                                                            │  │
│  │  ● React 19 (Modern) ✨ Recommended                        │  │
│  │    Better performance, modern features, future-proof       │  │
│  │                                                            │  │
│  └────────────────────────────────────────────────────────────┘  │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │  ⚠️ Migration Check Results                                │  │
│  │                                                            │  │
│  │  Found 2 issues that need attention:                       │  │
│  │                                                            │  │
│  │  🔴 CRITICAL (1)                                           │  │
│  │     └─ MyCustomComponent: componentWillMount removed       │  │
│  │                                                            │  │
│  │  🟡 WARNING (1)                                            │  │
│  │     └─ UserCard: defaultProps deprecated                   │  │
│  │                                                            │  │
│  │  [View Full Report]  [How to Fix]                          │  │
│  └────────────────────────────────────────────────────────────┘  │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │   Critical issues must be resolved before deploying      │  │
│  │     with React 19. You can still deploy with React 17.     │  │
│  └────────────────────────────────────────────────────────────┘  │
│                                                                  │
│            [Cancel]    [Deploy with React 17]    [Fix Issues]    │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

Migration Report Panel

┌──────────────────────────────────────────────────────────────────┐
│  Migration Report                                          [×]   │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Project: My Awesome App                                         │
│  Scanned: Dec 7, 2025 at 2:34 PM                                │
│                                                                  │
│  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  │
│                                                                  │
│  🔴 CRITICAL: componentWillMount removed                         │
│  ───────────────────────────────────────────────────────────     │
│  Location: Components/MyCustomComponent/Function Node            │
│                                                                  │
│  This lifecycle method has been completely removed in React 19.  │
│  Code using componentWillMount will throw an error at runtime.   │
│                                                                  │
│  Your code:                                                      │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │  componentWillMount() {                                    │  │
│  │    this.setState({ loading: true });                       │  │
│  │    this.loadData();                                        │  │
│  │  }                                                         │  │
│  └────────────────────────────────────────────────────────────┘  │
│                                                                  │
│  Recommended fix:                                                │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │  constructor(props) {                                      │  │
│  │    super(props);                                           │  │
│  │    this.state = { loading: true };                         │  │
│  │  }                                                         │  │
│  │                                                            │  │
│  │  componentDidMount() {                                     │  │
│  │    this.loadData();                                        │  │
│  │  }                                                         │  │
│  └────────────────────────────────────────────────────────────┘  │
│                                                                  │
│  [Go to Node]  [Copy Fix]  [Learn More ↗]                       │
│                                                                  │
│  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  │
│                                                                  │
│  🟡 WARNING: defaultProps deprecated                             │
│  ───────────────────────────────────────────────────────────     │
│  Location: Components/UserCard/Function Node                     │
│  ...                                                             │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

Implementation Phases

Phase 1: Infrastructure (Week 1-2)

Objective: Set up dual-build system without changing default behavior

  • Create separate webpack configs for React 17 and React 19 builds
  • Set up static/deploy-react19/ directory structure
  • Create React 19 versions of bundled React files
  • Update noodl-viewer-react/static/deploy/index.json to support version selection
  • Add runtime version metadata to deploy manifest

Success Criteria:

  • Both runtime versions build successfully
  • Default deploy still uses React 17
  • React 19 bundle available but not yet exposed in UI

Phase 2: Migration Detection (Week 2-3)

Objective: Build scanning and reporting system

  • Implement MigrationDetector class
  • Create pattern definitions for all known incompatibilities
  • Build project scanning logic
  • Generate human-readable migration reports
  • Add detection for custom React modules (external libs)

Success Criteria:

  • Scanner identifies all critical patterns in test projects
  • Reports clearly explain each issue with code examples
  • Scanner handles edge cases (minified code, JSX variations)

Phase 3: Editor Integration (Week 3-4)

Objective: Surface migration tools in the editor UI

  • Add runtime version selector to Deploy dialog
  • Integrate migration scanner with deploy workflow
  • Create Migration Report panel component
  • Add "Go to Node" navigation from report
  • Show inline warnings in JavaScript node editor

Success Criteria:

  • Users can select runtime version before deploy
  • Migration check runs automatically when React 19 selected
  • Clear UI prevents accidental broken deploys

Phase 4: Runtime Compatibility Layer (Week 4-5)

Objective: Update internal runtime code for React 19

  • Update noodl-viewer-react initialization to use createRoot()
  • Update SSR package to use hydrateRoot()
  • Migrate any internal componentWillMount usage
  • Update noodl-viewer-cloud for React 19
  • Test all built-in visual nodes with React 19

Success Criteria:

  • All built-in Noodl nodes work with React 19
  • SSR functions correctly with new APIs
  • No regressions in React 17 runtime

Phase 5: Documentation & Polish (Week 5-6)

Objective: Prepare for user adoption

  • Write migration guide for end users
  • Document all breaking changes with examples
  • Create video walkthrough of migration process
  • Add contextual help links in migration report
  • Beta test with community projects

Success Criteria:

  • Complete migration documentation
  • At least 5 community projects successfully migrated
  • No critical bugs in migration tooling

Technical Considerations

Build System Changes

// webpack-configs/webpack.deploy.config.js

const REACT_VERSION = process.env.REACT_VERSION || '17';

module.exports = {
  entry: `./src/init-react${REACT_VERSION}.js`,
  output: {
    path: path.resolve(__dirname, `../static/deploy${REACT_VERSION === '19' ? '-react19' : ''}`),
    filename: 'noodl.deploy.js'
  },
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM'
  },
  // ... rest of config
};

Runtime Initialization (React 19)

// src/init-react19.js

import { createRoot, hydrateRoot } from 'react-dom/client';

export function initializeApp(App, container, options = {}) {
  if (options.hydrate && container.hasChildNodes()) {
    return hydrateRoot(container, App);
  }
  
  const root = createRoot(container);
  root.render(App);
  return root;
}

export function unmountApp(root) {
  root.unmount();
}

// Expose for runtime
window.NoodlReactInit = { initializeApp, unmountApp };

Backwards Compatibility

// src/compat/react-compat.js

// Shim for code that might reference old APIs
if (typeof ReactDOM !== 'undefined' && !ReactDOM.render) {
  console.warn(
    '[Noodl] ReactDOM.render is not available in React 19. ' +
    'Please update your custom code to use createRoot instead.'
  );
  
  // Provide a helpful error instead of undefined function
  ReactDOM.render = () => {
    throw new Error(
      'ReactDOM.render has been removed in React 19. ' +
      'See migration guide: https://docs.opennoodl.com/migration/react19'
    );
  };
}

Success Criteria

Quantitative

  • 100% of built-in Noodl nodes work on React 19
  • Migration scanner detects >95% of incompatible patterns
  • Build time increase <10% for dual-runtime support
  • Zero regressions in React 17 runtime behavior

Qualitative

  • Users can confidently choose their runtime version
  • Migration report provides actionable guidance
  • No user is forced to migrate before they're ready
  • Documentation covers all common migration scenarios

Risks & Mitigations

Risk Impact Likelihood Mitigation
Custom modules with deep React dependencies High Medium Provide detection + detailed migration docs
Third-party npm packages incompatible Medium Medium Document known incompatible packages
SSR behavior differences between versions High Low Extensive SSR testing suite
Build size increase from dual bundles Low High Only ship selected version, not both
Community confusion about versions Medium Medium Clear UI, documentation, and defaults

Future Considerations

React 20+ Preparation

This dual-runtime architecture sets up a pattern for future React upgrades:

  • Version selection UI is extensible
  • Migration scanner patterns are configurable
  • Build system supports arbitrary version targets

Deprecation Timeline

v1.2.0 - React 19 available as opt-in (default: React 17)
v1.3.0 - React 19 becomes default (React 17 still available)
v1.4.0 - React 17 shows deprecation warning
v2.0.0 - React 17 support removed

Last Updated: December 7, 2025 Phase Owner: TBD Estimated Duration: 6 weeks