23 KiB
Task: React 19 Node Modernization
Overview
Update all frontend visual nodes in noodl-viewer-react to take advantage of React 19 features, remove deprecated patterns, and prepare the infrastructure for future React 19-only features like View Transitions.
Priority: High
Estimated Effort: 16-24 hours
Branch: feature/react19-node-modernization
Background
With the editor upgraded to React 19 and the runtime to React 18.3 (95% compatible), we have an opportunity to modernize our node infrastructure. This work removes technical debt, simplifies code, and prepares the foundation for React 19-exclusive features.
React 19 Changes That Affect Nodes
refas a regular prop - No moreforwardRefwrapper needed- Improved
useTransition- Can now handle async functions useDeferredValuewith initial value - New parameter for better loading states- Native document metadata -
<title>,<meta>render directly - Better Suspense - Works with more scenarios
use()hook - Read resources in render (promises, context)- Form actions -
useActionState,useFormStatus,useOptimistic - Cleaner cleanup - Ref cleanup functions
Phase 1: Infrastructure Updates
1.1 Update createNodeFromReactComponent Wrapper
File: packages/noodl-viewer-react/src/react-component-node.js (or .ts)
Changes:
- Remove automatic
forwardRefwrapping logic - Add support for
refas a standard prop - Add optional
useTransitionintegration for state updates - Add optional
useDeferredValuewrapper for specified inputs
New Options:
createNodeFromReactComponent({
// ... existing options
// NEW: React 19 options
react19: {
// Enable transition wrapping for specified inputs
transitionInputs: ['items', 'filter'],
// Enable deferred value for specified inputs
deferredInputs: ['searchQuery'],
// Enable form action support
formActions: true,
}
})
1.2 Update Base Node Classes
Files:
packages/noodl-viewer-react/src/nodes/std-library/visual-base.js- Any shared base classes for visual nodes
Changes:
- Remove
forwardRefpatterns - Update ref handling to use callback ref pattern
- Add utility methods for transitions:
this.startTransition(callback)- wrap updates in transitionthis.getDeferredValue(inputName)- get deferred version of input
1.3 Update TypeScript Definitions
Files:
packages/noodl-viewer-react/static/viewer/global.d.ts.keep- Any relevant
.d.tsfiles
Changes:
- Update component prop types to include
refas regular prop - Add types for new React 19 hooks
- Update
Noodlnamespace types if needed
Phase 2: Core Visual Nodes
2.1 Group Node
File: packages/noodl-viewer-react/src/nodes/std-library/group.js
Current Issues:
- Likely uses
forwardRefor class component with ref forwarding - May have legacy lifecycle patterns
Updates:
- Convert to functional component with
refas prop - Use
useEffectcleanup returns properly - Add optional
useDeferredValuefor children rendering (large lists)
New Capabilities:
Defer Childreninput (boolean) - usesuseDeferredValuefor smoother updatesIs Updatingoutput - true when deferred update pending
2.2 Text Node
File: packages/noodl-viewer-react/src/nodes/std-library/text.js
Updates:
- Remove
forwardRefwrapper - Simplify ref handling
2.3 Image Node
File: packages/noodl-viewer-react/src/nodes/std-library/image.js
Updates:
- Remove
forwardRefwrapper - Add resource preloading hints for React 19's
preload()API (future enhancement slot)
2.4 Video Node
File: packages/noodl-viewer-react/src/nodes/std-library/video.js
Updates:
- Remove
forwardRefwrapper - Ensure ref cleanup is proper
2.5 Circle Node
File: packages/noodl-viewer-react/src/nodes/std-library/circle.js
Updates:
- Remove
forwardRefwrapper
2.6 Icon Node
File: packages/noodl-viewer-react/src/nodes/std-library/icon.js (or net.noodl.visual.icon)
Updates:
- Remove
forwardRefwrapper
Phase 3: UI Control Nodes
3.1 Button Node
File: packages/noodl-viewer-react/src/nodes/std-library/button.js (or net.noodl.controls.button)
Updates:
- Remove
forwardRefwrapper - Add form action support preparation:
formActioninput (string) - for future form integrationIs Pendingoutput - when used in form with pending action
3.2 Text Input Node
File: packages/noodl-viewer-react/src/nodes/std-library/textinput.js
Updates:
- Remove
forwardRefwrapper - Consider
useDeferredValueforonChangevalue updates - Add form integration preparation
New Capabilities (Optional):
Defer Updatesinput - delaysValueoutput updates for performanceImmediate Valueoutput - non-deferred value for UI feedback
3.3 Checkbox Node
File: packages/noodl-viewer-react/src/nodes/std-library/checkbox.js
Updates:
- Remove
forwardRefwrapper - Add optimistic update preparation (
useOptimisticslot)
3.4 Radio Button / Radio Button Group
Files:
packages/noodl-viewer-react/src/nodes/std-library/radiobutton.jspackages/noodl-viewer-react/src/nodes/std-library/radiobuttongroup.js
Updates:
- Remove
forwardRefwrappers - Ensure proper group state management
3.5 Options/Dropdown Node
File: packages/noodl-viewer-react/src/nodes/std-library/options.js
Updates:
- Remove
forwardRefwrapper - Consider
useDeferredValuefor large option lists
3.6 Range/Slider Node
File: packages/noodl-viewer-react/src/nodes/std-library/range.js
Updates:
- Remove
forwardRefwrapper useDeferredValuefor value output (prevent render thrashing during drag)
New Capabilities:
Deferred Valueoutput - smoothed value for expensive downstream rendersImmediate Valueoutput - raw value for UI display
Phase 4: Navigation Nodes
4.1 Page Router / Router Node
File: packages/noodl-viewer-react/src/nodes/std-library/router.js
Updates:
- Add
useTransitionwrapping for navigation - Prepare for View Transitions API integration
New Capabilities:
Is Transitioningoutput - true during page transitionUse Transitioninput (boolean, default true) - wrap navigation in React transition
4.2 Router Navigate Node
File: packages/noodl-viewer-react/src/nodes/std-library/routernavigate.js
Updates:
- Wrap navigation in
startTransition
New Capabilities:
Is Pendingoutput - navigation in progressTransition Priorityinput (enum: 'normal', 'urgent') - for future prioritization
4.3 Page Stack / Component Stack
File: packages/noodl-viewer-react/src/nodes/std-library/pagestack.js
Updates:
- Add
useTransitionfor push/pop operations
New Capabilities:
Is Transitioningoutput- Prepare for animation coordination with View Transitions
4.4 Page Inputs Node
File: packages/noodl-viewer-react/src/nodes/std-library/pageinputs.js
Updates:
- Standard cleanup, ensure no deprecated patterns
4.5 Popup Nodes
Files:
packages/noodl-viewer-react/src/nodes/std-library/showpopup.jspackages/noodl-viewer-react/src/nodes/std-library/closepopup.js
Updates:
- Consider
useTransitionfor popup show/hide
Phase 5: Layout Nodes
5.1 Columns Node
File: packages/noodl-viewer-react/src/nodes/std-library/columns.js
Updates:
- Remove
forwardRefwrapper - Remove
React.cloneElementif present (React 19 has better patterns) - Consider using CSS Grid native features
5.2 Repeater (For Each) Node
File: packages/noodl-viewer-react/src/nodes/std-library/foreach.js
Critical Updates:
- Add
useDeferredValuefor items array - Add
useTransitionfor item updates
New Capabilities:
Defer Updatesinput (boolean) - uses deferred value for itemsIs Updatingoutput - true when deferred update pendingTransition Updatesinput (boolean) - wrap updates in transition
Why This Matters: Large list updates currently cause jank. With these options:
- User toggles
Defer Updates→ list updates don't block UI Is Updatingoutput → can show loading indicator
5.3 Component Children Node
File: packages/noodl-viewer-react/src/nodes/std-library/componentchildren.js
Updates:
- Standard cleanup
Phase 6: Data/Object Nodes
6.1 Component Object Node
File: packages/noodl-viewer-react/src/nodes/std-library/componentobject.js
Updates:
- Consider context-based implementation for React 19
use(Context)can now be called conditionally in React 19
6.2 Parent Component Object Node
File: Similar location
Updates:
- Same as Component Object
Phase 7: SEO/Document Nodes (New Capability)
7.1 Update Page Node for Document Metadata
File: packages/noodl-viewer-react/src/nodes/std-library/page.js
New Capabilities:
React 19 allows rendering <title>, <meta>, <link> directly in components and they hoist to <head>.
New Inputs:
Page Title- renders<title>(already exists, but implementation changes)Meta Description- renders<meta name="description">Meta Keywords- renders<meta name="keywords">Canonical URL- renders<link rel="canonical">OG Title- renders<meta property="og:title">OG Description- renders<meta property="og:description">OG Image- renders<meta property="og:image">
Implementation:
function PageComponent({ title, description, ogTitle, ...props }) {
return (
<>
{title && <title>{title}</title>}
{description && <meta name="description" content={description} />}
{ogTitle && <meta property="og:title" content={ogTitle} />}
{/* ... rest of component */}
</>
);
}
This replaces the hacky SSR string replacement currently in packages/noodl-viewer-react/static/ssr/index.js.
Phase 8: Testing & Validation
8.1 Unit Tests
Update/Create Tests For:
createNodeFromReactComponentwith new options- Each updated node renders correctly
- Ref forwarding works without
forwardRef - Deferred values update correctly
- Transitions wrap updates properly
8.2 Integration Tests
- Page navigation with transitions
- Repeater with large datasets
- Form interactions with new patterns
8.3 Visual Regression Tests
- Ensure no visual changes from modernization
- Test all visual states (hover, pressed, disabled)
- Test variants still work
8.4 Performance Benchmarks
Before/After Metrics:
- Repeater with 1000 items - render time
- Page navigation - transition smoothness
- Text input rapid typing - lag measurement
File List Summary
Infrastructure Files
packages/noodl-viewer-react/src/
├── react-component-node.js # Main wrapper factory
├── nodes/std-library/
│ └── visual-base.js # Base class for visual nodes
Visual Element Nodes
packages/noodl-viewer-react/src/nodes/std-library/
├── group.js
├── text.js
├── image.js
├── video.js
├── circle.js
├── icon.js (or net.noodl.visual.icon)
UI Control Nodes
packages/noodl-viewer-react/src/nodes/std-library/
├── button.js (or net.noodl.controls.button)
├── textinput.js
├── checkbox.js
├── radiobutton.js
├── radiobuttongroup.js
├── options.js
├── range.js
Navigation Nodes
packages/noodl-viewer-react/src/nodes/std-library/
├── router.js
├── routernavigate.js
├── pagestack.js
├── pageinputs.js
├── showpopup.js
├── closepopup.js
Layout Nodes
packages/noodl-viewer-react/src/nodes/std-library/
├── columns.js
├── foreach.js
├── componentchildren.js
Data Nodes
packages/noodl-viewer-react/src/nodes/std-library/
├── componentobject.js
├── parentcomponentobject.js
Page/SEO Nodes
packages/noodl-viewer-react/src/nodes/std-library/
├── page.js
Type Definitions
packages/noodl-viewer-react/static/viewer/
├── global.d.ts.keep
Implementation Order
Week 1: Foundation
- Update
createNodeFromReactComponentinfrastructure - Update base classes
- Update Group node (most used, good test case)
- Update Text node
- Create test suite for modernized patterns
Week 2: Controls & Navigation
- Update all UI Control nodes (Button, TextInput, etc.)
- Update Navigation nodes with transition support
- Update Repeater with deferred value support
- Test navigation flow end-to-end
Week 3: Polish & New Features
- Update remaining nodes (Columns, Component Object, etc.)
- Add Page metadata support
- Performance testing and optimization
- Documentation updates
Success Criteria
Must Have
- All nodes render correctly after updates
- No
forwardRefusage in visual nodes - All refs work correctly (DOM access, focus, etc.)
- No breaking changes to existing projects
- Tests pass
Should Have
- Repeater has
Defer Updatesoption - Page Router has
Is Transitioningoutput - Page node has SEO metadata inputs
Nice to Have
- Performance improvement measurable in benchmarks
- Text Input deferred value option
- Range slider deferred value option
Migration Notes
Backward Compatibility
These changes should be fully backward compatible:
- Existing projects continue to work unchanged
- New features are opt-in via new inputs
- No changes to how nodes are wired together
Runtime Considerations
Since runtime is React 18.3:
useTransitionworks (available since React 18)useDeferredValueworks (available since React 18)refas prop works (React 18.3 forward-ported this)- Native metadata hoisting does NOT work (React 19 only)
- For runtime, metadata nodes will need polyfill/fallback
Strategy: Build features for React 19 editor, provide graceful degradation for React 18.3 runtime. Eventually upgrade runtime to React 19.
Code Examples
Before: forwardRef Pattern
getReactComponent() {
return React.forwardRef((props, ref) => {
return <div ref={ref} style={props.style}>{props.children}</div>;
});
}
After: ref as Prop Pattern
getReactComponent() {
return function GroupComponent({ ref, style, children }) {
return <div ref={ref} style={style}>{children}</div>;
};
}
Adding Deferred Value Support
getReactComponent() {
return function RepeaterComponent({ items, deferUpdates, onIsUpdating }) {
const deferredItems = React.useDeferredValue(items);
const isStale = items !== deferredItems;
React.useEffect(() => {
onIsUpdating?.(isStale);
}, [isStale, onIsUpdating]);
const itemsToRender = deferUpdates ? deferredItems : items;
return (
<div>
{itemsToRender.map(item => /* render item */)}
</div>
);
};
}
Adding Transition Support
getReactComponent() {
return function RouterComponent({ onNavigate, onIsTransitioning }) {
const [isPending, startTransition] = React.useTransition();
React.useEffect(() => {
onIsTransitioning?.(isPending);
}, [isPending, onIsTransitioning]);
const handleNavigate = (target) => {
startTransition(() => {
onNavigate(target);
});
};
// ...
};
}
Questions for Implementation
-
File locations: Need to verify actual file paths in
noodl-viewer-react- the paths above are educated guesses based on patterns. -
Runtime compatibility: Should we add feature detection to gracefully degrade on React 18.3 runtime, or assume eventual runtime upgrade?
-
New inputs/outputs: Should new capabilities (like
Defer Updates) be hidden by default and exposed via a "React 19 Features" toggle in project settings? -
Breaking changes policy: If we find any patterns that would break (unlikely), what's the policy? Migration path vs versioning?
Related Future Work
This modernization enables but does not include:
- Magic Transition Node - View Transitions API wrapper
- AI Component Node - Generative UI with streaming
- Async Boundary Node - Suspense wrapper with error boundaries
- Form Action Node - React 19 form actions
These will be separate tasks building on this foundation.
React 19 Node Modernization - Implementation Checklist
Quick reference checklist for implementation. See full spec for details.
Pre-Flight Checks
- Verify React 19 is installed in editor package
- Verify React 18.3 is installed in runtime package
- Create feature branch:
feature/react19-node-modernization - Locate all node files in
packages/noodl-viewer-react/src/nodes/
Phase 1: Infrastructure
createNodeFromReactComponent
- Find file:
packages/noodl-viewer-react/src/react-component-node.js - Remove automatic forwardRef wrapping
- Add
refprop passthrough to components - Add optional
react19.transitionInputsconfig - Add optional
react19.deferredInputsconfig - Test: Basic node still renders
- Test: Ref forwarding works
Base Classes
- Find visual-base.js or equivalent
- Add
this.startTransition()utility method - Add
this.getDeferredValue()utility method - Update TypeScript definitions if applicable
Phase 2: Core Visual Nodes
Group Node
- Remove forwardRef
- Use
refas regular prop - Test: Renders correctly
- Test: Ref accessible for DOM manipulation
- Optional: Add
Defer Childreninput - Optional: Add
Is Updatingoutput
Text Node
- Remove forwardRef
- Test: Renders correctly
Image Node
- Remove forwardRef
- Test: Renders correctly
Video Node
- Remove forwardRef
- Ensure proper ref cleanup
- Test: Renders correctly
Circle Node
- Remove forwardRef
- Test: Renders correctly
Icon Node
- Remove forwardRef
- Test: Renders correctly
Phase 3: UI Control Nodes
Button Node
- Remove forwardRef
- Test: Click events work
- Test: Visual states work (hover, pressed, disabled)
- Optional: Add
Is Pendingoutput for forms
Text Input Node
- Remove forwardRef
- Test: Value binding works
- Test: Focus/blur events work
- Optional: Add
Defer Updatesinput - Optional: Add
Immediate Valueoutput
Checkbox Node
- Remove forwardRef
- Test: Checked state works
Radio Button Node
- Remove forwardRef
- Test: Selection works
Radio Button Group Node
- Remove forwardRef
- Test: Group behavior works
Options/Dropdown Node
- Remove forwardRef
- Test: Selection works
- Optional: useDeferredValue for large option lists
Range/Slider Node
- Remove forwardRef
- Test: Value updates work
- Optional: Add
Deferred Valueoutput - Optional: Add
Immediate Valueoutput
Phase 4: Navigation Nodes
Router Node
- Remove forwardRef if present
- Add useTransition for navigation
- Add
Is Transitioningoutput - Test: Page navigation works
- Test: Is Transitioning output fires correctly
Router Navigate Node
- Wrap navigation in startTransition
- Add
Is Pendingoutput - Test: Navigation triggers correctly
Page Stack Node
- Add useTransition for push/pop
- Add
Is Transitioningoutput - Test: Stack operations work
Page Inputs Node
- Standard cleanup
- Test: Parameters pass correctly
Show Popup Node
- Consider useTransition
- Test: Popup shows/hides
Close Popup Node
- Standard cleanup
- Test: Popup closes
Phase 5: Layout Nodes
Columns Node
- Remove forwardRef
- Remove React.cloneElement if present
- Test: Column layout works
Repeater (For Each) Node ⭐ HIGH VALUE
- Remove forwardRef if present
- Add useDeferredValue for items
- Add useTransition for updates
- Add
Defer Updatesinput - Add
Is Updatingoutput - Add
Transition Updatesinput - Test: Basic rendering works
- Test: Large list performance improved
- Test: Is Updating output fires correctly
Component Children Node
- Standard cleanup
- Test: Children render correctly
Phase 6: Data Nodes
Component Object Node
- Review implementation
- Consider React 19 context patterns
- Test: Object access works
Parent Component Object Node
- Same as Component Object
- Test: Parent access works
Phase 7: Page/SEO Node ⭐ HIGH VALUE
Page Node
- Add
Page Titleinput → renders<title> - Add
Meta Descriptioninput → renders<meta name="description"> - Add
Canonical URLinput → renders<link rel="canonical"> - Add
OG Titleinput → renders<meta property="og:title"> - Add
OG Descriptioninput - Add
OG Imageinput - Test: Metadata renders in head
- Test: SSR works correctly
- Provide fallback for React 18.3 runtime
Phase 8: Testing
Unit Tests
- createNodeFromReactComponent tests
- Ref forwarding tests
- Deferred value tests
- Transition tests
Integration Tests
- Full navigation flow
- Repeater with large data
- Form interactions
Visual Tests
- All nodes render same as before
- Visual states work
- Variants work
Performance Tests
- Benchmark: Repeater 1000 items
- Benchmark: Page navigation
- Benchmark: Text input typing
Final Steps
- Update documentation
- Update changelog
- Create PR
- Test in sample projects
- Deploy to staging
- User testing
Quick Reference: Pattern Changes
forwardRef Removal
Before:
React.forwardRef((props, ref) => <div ref={ref} />)
After:
function Component({ ref, ...props }) { return <div ref={ref} /> }
Adding Deferred Value
function Component({ items, deferUpdates, onIsUpdating }) {
const deferredItems = React.useDeferredValue(items);
const isStale = items !== deferredItems;
React.useEffect(() => {
onIsUpdating?.(isStale);
}, [isStale]);
return /* render deferUpdates ? deferredItems : items */;
}
Adding Transitions
function Component({ onNavigate, onIsPending }) {
const [isPending, startTransition] = React.useTransition();
React.useEffect(() => {
onIsPending?.(isPending);
}, [isPending]);
const handleNav = (target) => {
startTransition(() => onNavigate(target));
};
}
Document Metadata (React 19)
function Page({ title, description }) {
return (
<>
{title && <title>{title}</title>}
{description && <meta name="description" content={description} />}
{/* rest of page */}
</>
);
}
Notes
- High value items marked with ⭐
- Start with infrastructure, then Group node as test case
- Test frequently - small iterations
- Keep backward compatibility - no breaking changes