Skip to main content

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:

Transform input values

function UppercaseEditor() {
    const [text, setText] = useState('');

    const handleChange = (value: string) => {
        // Transform the value before storing
        setText(value.toUpperCase());
    };

    return (
        <DyeLight
            value={text}
            onChange={handleChange}
        />
    );
}

Validate input

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:

Optimize performance

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>
        </>
    );
}

Simple forms without validation

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.

Performance comparison

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

Best practices

  1. Default to controlled mode unless performance is a concern
  2. Use uncontrolled mode for simple forms that only need the value on submit
  3. Don’t mix modes in the same component instance
  4. Use refs for programmatic control regardless of mode
  5. Consider performance - uncontrolled mode prevents parent re-renders

Next steps

Build docs developers (and LLMs) love