Documentation Index
Fetch the complete documentation index at: https://mintlify.com/ragaeeb/dyelight/llms.txt
Use this file to discover all available pages before exploring further.
Like standard React form elements, DyeLight supports both controlled and uncontrolled usage patterns. This guide explains the differences and when to use each.
Controlled mode
In controlled mode, you manage the component’s value through React state:
import { useState } from 'react';
import { DyeLight } from 'dyelight';
function ControlledEditor() {
const [text, setText] = useState('');
return (
<DyeLight
value={text} // Controlled value
onChange={setText} // Update handler
highlights={[]}
/>
);
}
Key characteristics:
- You provide the
value prop
- You handle the
onChange callback
- React state is the source of truth
- You have full control over the value
When using controlled mode, the component checks if value !== undefined. If true, it operates in controlled mode.
Uncontrolled mode
In uncontrolled mode, the component manages its own internal state:
import { useRef } from 'react';
import { DyeLight, type DyeLightRef } from 'dyelight';
function UncontrolledEditor() {
const editorRef = useRef<DyeLightRef>(null);
const handleSubmit = () => {
const value = editorRef.current?.getValue();
console.log('Submitted:', value);
};
return (
<>
<DyeLight
ref={editorRef}
defaultValue="Initial text" // Uncontrolled default
highlights={[]}
/>
<button onClick={handleSubmit}>Submit</button>
</>
);
}
Key characteristics:
- You provide
defaultValue instead of value
- The component manages its own state
- Access the current value via ref methods
- Less re-renders in your parent component
When to use controlled mode
Use controlled mode when you need to:
function UppercaseEditor() {
const [text, setText] = useState('');
const handleChange = (value: string) => {
// Transform the value before storing
setText(value.toUpperCase());
};
return (
<DyeLight
value={text}
onChange={handleChange}
/>
);
}
function LimitedEditor() {
const [text, setText] = useState('');
const MAX_LENGTH = 100;
const handleChange = (value: string) => {
// Only update if within limit
if (value.length <= MAX_LENGTH) {
setText(value);
}
};
return (
<>
<DyeLight
value={text}
onChange={handleChange}
/>
<p>{text.length} / {MAX_LENGTH}</p>
</>
);
}
Synchronize with other components
function SyncedEditors() {
const [sharedText, setSharedText] = useState('');
return (
<div className="grid grid-cols-2 gap-4">
<DyeLight
value={sharedText}
onChange={setSharedText}
/>
<DyeLight
value={sharedText}
onChange={setSharedText}
/>
</div>
);
}
React to every change
function LivePreview() {
const [markdown, setMarkdown] = useState('# Hello');
return (
<div className="grid grid-cols-2 gap-4">
<DyeLight
value={markdown}
onChange={setMarkdown}
/>
<MarkdownPreview content={markdown} />
</div>
);
}
When to use uncontrolled mode
Use uncontrolled mode when you need to:
function PerformantForm() {
const editorRef = useRef<DyeLightRef>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const content = editorRef.current?.getValue() || '';
// Submit to server
api.saveContent(content);
};
return (
<form onSubmit={handleSubmit}>
<DyeLight
ref={editorRef}
defaultValue="Initial content"
/>
<button type="submit">Save</button>
</form>
);
}
This avoids re-rendering the parent component on every keystroke.
Read value only on submit
function CommentForm() {
const commentRef = useRef<DyeLightRef>(null);
const postComment = async () => {
const comment = commentRef.current?.getValue();
if (comment) {
await api.post('/comments', { text: comment });
// Clear the editor
commentRef.current?.setValue('');
}
};
return (
<>
<DyeLight
ref={commentRef}
defaultValue=""
placeholder="Write a comment..."
/>
<button onClick={postComment}>Post</button>
</>
);
}
function SimpleNoteEditor() {
const noteRef = useRef<DyeLightRef>(null);
return (
<DyeLight
ref={noteRef}
defaultValue="My notes..."
enableAutoResize
/>
);
}
Using refs with controlled mode
You can use refs even in controlled mode for programmatic control:
function ControlledWithRef() {
const [text, setText] = useState('');
const editorRef = useRef<DyeLightRef>(null);
const handleFocus = () => {
editorRef.current?.focus();
};
const handleSelectAll = () => {
editorRef.current?.select();
};
const handleClear = () => {
setText(''); // Update state
editorRef.current?.focus();
};
return (
<>
<DyeLight
ref={editorRef}
value={text}
onChange={setText}
/>
<div className="flex gap-2">
<button onClick={handleFocus}>Focus</button>
<button onClick={handleSelectAll}>Select All</button>
<button onClick={handleClear}>Clear</button>
</div>
</>
);
}
Available ref methods
Both controlled and uncontrolled modes expose these ref methods:
type DyeLightRef = {
focus: () => void; // Focus the editor
blur: () => void; // Remove focus
select: () => void; // Select all text
setSelectionRange: (start: number, end: number) => void; // Set selection
getValue: () => string; // Get current value
setValue: (value: string) => void; // Set value programmatically
scrollToPosition: (pos: number, offset?: number, behavior?: ScrollBehavior) => void;
exportForAI: () => string; // Export debug report (requires debug={true})
};
Example usage:
const editorRef = useRef<DyeLightRef>(null);
// Get current value
const value = editorRef.current?.getValue();
// Set a new value
editorRef.current?.setValue('New content');
// Set cursor position
editorRef.current?.setSelectionRange(0, 5);
// Scroll to a highlight
editorRef.current?.scrollToPosition(100, 40, 'smooth');
Switching between modes
Do not switch between controlled and uncontrolled modes during the component’s lifetime. Choose one pattern and stick with it.
// ❌ Bad: Switching modes
function BadExample() {
const [mode, setMode] = useState<'controlled' | 'uncontrolled'>('controlled');
const [text, setText] = useState('');
return (
<DyeLight
value={mode === 'controlled' ? text : undefined}
defaultValue={mode === 'uncontrolled' ? text : undefined}
onChange={setText}
/>
);
}
// ✅ Good: Consistent mode
function GoodExample() {
const [text, setText] = useState('');
return (
<DyeLight
value={text}
onChange={setText}
/>
);
}
How DyeLight determines the mode
The component checks the value prop:
// From DyeLight.tsx
const isControlled = value !== undefined;
- If
value is provided (even if it’s an empty string), the component is controlled
- If
value is undefined, the component is uncontrolled
This matches React’s standard behavior for form elements.
Pros:
- Full control over value
- Easy to validate and transform
- Simple to synchronize with other state
Cons:
- Parent re-renders on every keystroke
- Slightly more code
- Can impact performance with large components
Pros:
- Better performance (fewer re-renders)
- Less boilerplate code
- Good for simple forms
Cons:
- Less control over value
- Harder to validate during input
- Requires refs to access value
Best practices
- Default to controlled mode unless performance is a concern
- Use uncontrolled mode for simple forms that only need the value on submit
- Don’t mix modes in the same component instance
- Use refs for programmatic control regardless of mode
- Consider performance - uncontrolled mode prevents parent re-renders
Next steps