-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
Provide environment information
System:
OS: Windows 11 10.0.26100
CPU: (12) x64 13th Gen Intel(R) Core(TM) i5-13420H
Memory: 6.36 GB / 15.71 GB
Binaries:
Node: 22.15.0 - C:\Program Files\nodejs\node.EXE
npm: 10.9.2 - C:\Program Files\nodejs\npm.CMD
pnpm: 10.12.3 - ~\AppData\Roaming\npm\pnpm.CMD
Novel Editor Version: "novel": "^1.0.2"
React Version: "react": "^18.3.1"
Framework: React with TypeScript, Shadcn ui and Tailwind Css
Use Case: Form builder with custom NodeView components
Describe the bug
Summary
Custom NodeView components lose input focus after each keystroke, requiring users to click back into the input field for every character typed. This makes form building completely unusable for custom form elements.
Problem Description
When typing in input fields within custom NodeView components (React components), the input loses focus after each keystroke. Users can only type one character at a time before having to click back into the input field.
Affected Components
- Custom NodeView components with input fields (e.g., form elements like ShortAnswer, MultipleChoice, etc.)
- Built-in editor elements (headings, paragraphs) work perfectly fine
Steps to Reproduce
- Create a custom NodeView component with an input field:
export const ShortAnswerComponent = ({ node, updateAttributes, editor }: NodeViewProps) => {
const [localLabel, setLocalLabel] = useState(node.attrs.label || "");
return (
<NodeViewWrapper>
<Input
value={localLabel}
onChange={(e) => {
setLocalLabel(e.target.value);
updateAttributes({ label: e.target.value });
}}
placeholder="Type a question"
/>
</NodeViewWrapper>
);
};
- Register the extension and use it in the editor
- Try typing continuously in the input field
- Observe that focus is lost after each character
Expected Behavior
Users should be able to type continuously in custom NodeView input fields without losing focus.
Actual Behavior
Input focus is lost after each keystroke, requiring manual refocusing for each character.
Investigation Findings
Through extensive debugging, we identified the root cause:
Primary Issue: Editor Recreation
The editor instance is being recreated on every content change, which destroys and rebuilds all NodeView components, causing focus loss.
Evidence from logs:
🎉 Editor onCreate triggered (multiple times per keystroke)
🏗️ Editor component rendering
🏗️ Initial value changed: true
🔧 Editor ref changed: null -> Editor instance
Contributing Factors:
- Changing
initialValue
prop: When the editor'sinitialValue
prop changes, it triggers a complete recreation - Unstable
onChange
callbacks: Parent component recreating theonChange
function on every render - NodeView re-rendering: Custom components re-rendering excessively due to attribute updates
Technical Analysis
Root Cause Chain:
- User types in NodeView input →
updateAttributes()
called - Editor content changes →
onUpdate
callback triggered - Parent component updates state → Editor props change
- Editor receives new
initialValue
oronChange
→ Editor recreates - All NodeViews destroyed and rebuilt → Focus lost
Key Logs Pattern:
ShortAnswerComponent.tsx:20 🎯 ShortAnswerComponent rendering (4+ times per keystroke)
Editor.tsx:84 🎉 Editor onCreate triggered (editor recreation)
Editor.tsx:41 🏗️ Initial value changed: true (prop change detected)
FormBuilder.tsx:91 🔧 Editor ref changed: null (editor unmounted)
Attempted Solutions
We tried multiple approaches:
❌ Failed Approaches:
- Debouncing
updateAttributes
calls - Using
React.memo
on NodeView components - Focus management with refs and cursor position tracking
- Preventing content state updates during typing
✅ Partial Success:
- Stabilizing the
initialValue
prop (preventing it from changing) - Memoizing the
onChange
callback with stable dependencies - Conditional editor rendering to prevent null→content transitions
Workaround Implementation
The most effective workaround involves:
- Stable Initial Value Management:
const [stableInitialValue, setStableInitialValue] = useState(null);
const hasSetStableInitialValue = useRef(false);
// Set once, never change
useEffect(() => {
if (!hasSetStableInitialValue.current) {
setStableInitialValue(defaultContent);
hasSetStableInitialValue.current = true;
}
}, []);
- Stable onChange Callback:
const handleContentChange = useCallback((jsonContent, htmlContent) => {
// Store in ref instead of state to prevent re-renders
latestContentRef.current = jsonContent;
setHtmlContent(htmlContent);
}, []); // Empty dependencies
- Conditional Editor Rendering:
{stableInitialValue ? (
<Editor
key="stable-editor"
initialValue={stableInitialValue}
onChange={handleContentChange}
/>
) : (
<LoadingSpinner />
)}
Suggested Fix
The issue seems to stem from how Novel handles prop changes. Consider:
- Internal State Management: Editor should maintain its own internal state and not recreate when
initialValue
changes after initialization - Stable NodeView Lifecycle: Ensure NodeView components aren't destroyed/recreated during normal content updates
- Prop Change Optimization: Differentiate between "initial setup" and "content updates" to avoid unnecessary recreation
Impact
This issue makes Novel editor unusable for any form builder or application requiring custom NodeView components with user input, which is a common use case for rich text editors.
Link to reproduction
https://github.com/Ghufran496/MetanoaEditor
To reproduce
Steps to Reproduce
- Create a custom NodeView component with an input field:
export const ShortAnswerComponent = ({ node, updateAttributes, editor }: NodeViewProps) => {
const [localLabel, setLocalLabel] = useState(node.attrs.label || "");
return (
<NodeViewWrapper>
<Input
value={localLabel}
onChange={(e) => {
setLocalLabel(e.target.value);
updateAttributes({ label: e.target.value });
}}
placeholder="Type a question"
/>
</NodeViewWrapper>
);
};
- Register the extension and use it in the editor
- Try typing continuously in the input field
- Observe that focus is lost after each character
Additional information
No response