# 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
```javascript
// 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(, document.getElementById('root'));`,
after: `import { createRoot } from 'react-dom/client';\nconst root = createRoot(document.getElementById('root'));\nroot.render();`
}
},
{
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: ``,
after: `// Using createRef:\nmyInputRef = React.createRef();\n\n\n// Using callback ref:\n 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
```javascript
// 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)
```javascript
// 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
```javascript
// 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
```
## Related Documentation
- [React 19 Official Upgrade Guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide)
- [TASK-001: Dependency Updates & React 19 Migration (Editor)](./TASK-001-dependency-updates.md)
- [OpenNoodl Architecture Overview](./architecture/overview.md)
---
*Last Updated: December 7, 2025*
*Phase Owner: TBD*
*Estimated Duration: 6 weeks*