React components and hooks for building customizable rich text editors using the Slate framework.
—
Advanced hooks and utilities for optimizing editor performance, especially important for large documents. These features help prevent unnecessary re-renders and improve the user experience with complex or large-scale editor implementations.
The useSlateSelector hook provides Redux-style selectors to prevent unnecessary component re-renders by only updating when specific parts of the editor state change.
/**
* Use redux-style selectors to prevent re-rendering on every keystroke
* @param selector - Function to select specific data from editor
* @param equalityFn - Optional custom equality function for comparison
* @param options - Optional configuration options
* @returns Selected value from editor state
*/
function useSlateSelector<T>(
selector: (editor: Editor) => T,
equalityFn?: (a: T | null, b: T) => boolean,
options?: SlateSelectorOptions
): T;
/**
* Options for slate selector hooks
*/
interface SlateSelectorOptions {
/**
* If true, defer calling the selector function until after `Editable` has
* finished rendering. This ensures that `ReactEditor.findPath` won't return
* an outdated path if called inside the selector.
*/
deferred?: boolean;
}Usage Examples:
import React from 'react';
import { useSlateSelector, useSlateStatic } from 'slate-react';
import { Editor, Node } from 'slate';
// Select only word count - only re-renders when word count changes
const WordCounter = () => {
const wordCount = useSlateSelector((editor) => {
const text = Node.string(editor);
return text.split(/\s+/).filter(word => word.length > 0).length;
});
return <div>Words: {wordCount}</div>;
};
// Select only selection state - only re-renders when selection changes
const SelectionInfo = () => {
const selectionInfo = useSlateSelector((editor) => {
if (!editor.selection) return null;
return {
isCollapsed: Range.isCollapsed(editor.selection),
path: editor.selection.anchor.path.join(',')
};
});
if (!selectionInfo) return <div>No selection</div>;
return (
<div>
Selection: {selectionInfo.isCollapsed ? 'Cursor' : 'Range'}
at [{selectionInfo.path}]
</div>
);
};
// Custom equality function for complex objects
const BlockInfo = () => {
const blockInfo = useSlateSelector(
(editor) => {
const blocks = Array.from(Editor.nodes(editor, {
match: n => Element.isElement(n) && Editor.isBlock(editor, n)
}));
return { count: blocks.length, types: blocks.map(([n]) => n.type) };
},
// Custom equality function - only update if count or types change
(prev, curr) => {
if (!prev || prev.count !== curr.count) return false;
return JSON.stringify(prev.types) === JSON.stringify(curr.types);
}
);
return (
<div>
Blocks: {blockInfo.count}
Types: {blockInfo.types.join(', ')}
</div>
);
};
// Deferred updates for expensive calculations
const ExpensiveCalculation = () => {
const result = useSlateSelector(
(editor) => {
// Expensive calculation that might block UI
return performExpensiveAnalysis(editor);
},
undefined,
{ deferred: true } // Defer updates to prevent blocking
);
return <div>Analysis: {result}</div>;
};Slate React includes a chunking system that optimizes rendering of large documents by breaking them into smaller, manageable pieces.
/**
* Chunk-related types for performance optimization
*/
interface ChunkTree {
// Internal tree structure for optimizing large document rendering
}
type Chunk = ChunkLeaf | ChunkAncestor;
type ChunkLeaf = { type: 'leaf'; node: Node };
type ChunkAncestor = { type: 'ancestor'; children: Chunk[] };
type ChunkDescendant = ChunkLeaf | ChunkAncestor;The chunking system is automatically handled by Slate React, but you can customize chunk rendering with the renderChunk prop:
import React from 'react';
import { Editable, RenderChunkProps } from 'slate-react';
const optimizedRenderChunk = ({ attributes, children, highest, lowest }: RenderChunkProps) => {
// Add performance monitoring or custom styling for chunks
return (
<div
{...attributes}
className={`chunk ${highest ? 'highest' : ''} ${lowest ? 'lowest' : ''}`}
>
{children}
</div>
);
};
const LargeDocumentEditor = () => (
<Editable
renderChunk={optimizedRenderChunk}
// Other props...
/>
);Monitor editor performance and identify bottlenecks:
import React, { useCallback, useEffect } from 'react';
import { useSlateSelector, useSlateStatic } from 'slate-react';
const PerformanceMonitor = () => {
const editor = useSlateStatic();
// Monitor selection changes with performance timing
const selectionMetrics = useSlateSelector((editor) => {
const start = performance.now();
const selection = editor.selection;
const end = performance.now();
return {
selection,
selectionTime: end - start,
timestamp: Date.now()
};
});
// Monitor document size changes
const documentMetrics = useSlateSelector((editor) => {
const start = performance.now();
const nodeCount = Array.from(Node.nodes(editor)).length;
const textLength = Node.string(editor).length;
const end = performance.now();
return {
nodeCount,
textLength,
calculationTime: end - start
};
});
useEffect(() => {
console.log('Performance metrics:', {
selection: selectionMetrics.selectionTime,
document: documentMetrics.calculationTime
});
}, [selectionMetrics, documentMetrics]);
return (
<div>
<div>Nodes: {documentMetrics.nodeCount}</div>
<div>Characters: {documentMetrics.textLength}</div>
<div>Calc Time: {documentMetrics.calculationTime.toFixed(2)}ms</div>
</div>
);
};Optimize render functions with React.memo and useMemo:
import React, { memo, useMemo } from 'react';
import { RenderElementProps, RenderLeafProps } from 'slate-react';
// Memoized element renderer
const OptimizedElement = memo(({ attributes, children, element }: RenderElementProps) => {
const className = useMemo(() => {
return `element-${element.type} ${element.active ? 'active' : ''}`;
}, [element.type, element.active]);
return (
<div {...attributes} className={className}>
{children}
</div>
);
});
// Memoized leaf renderer with complex formatting
const OptimizedLeaf = memo(({ attributes, children, leaf }: RenderLeafProps) => {
const style = useMemo(() => {
const styles: React.CSSProperties = {};
if (leaf.fontSize) styles.fontSize = `${leaf.fontSize}px`;
if (leaf.color) styles.color = leaf.color;
if (leaf.backgroundColor) styles.backgroundColor = leaf.backgroundColor;
return styles;
}, [leaf.fontSize, leaf.color, leaf.backgroundColor]);
const formattedChildren = useMemo(() => {
let result = children;
if (leaf.bold) result = <strong>{result}</strong>;
if (leaf.italic) result = <em>{result}</em>;
if (leaf.underline) result = <u>{result}</u>;
return result;
}, [children, leaf.bold, leaf.italic, leaf.underline]);
return (
<span {...attributes} style={style}>
{formattedChildren}
</span>
);
});Implement virtual scrolling for documents with thousands of elements:
import React, { useMemo, useState, useEffect } from 'react';
import { useSlateSelector } from 'slate-react';
import { Editor, Node, Element } from 'slate';
const VirtualizedEditor = () => {
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 100 });
// Only select visible nodes to minimize re-renders
const visibleNodes = useSlateSelector((editor) => {
const allNodes = Array.from(Editor.nodes(editor, {
match: n => Element.isElement(n)
}));
return allNodes.slice(visibleRange.start, visibleRange.end);
});
const handleScroll = (event: React.UIEvent) => {
const target = event.target as HTMLElement;
const scrollTop = target.scrollTop;
const itemHeight = 50; // Estimated item height
const containerHeight = target.clientHeight;
const start = Math.floor(scrollTop / itemHeight);
const end = start + Math.ceil(containerHeight / itemHeight) + 5; // Buffer
setVisibleRange({ start, end });
};
return (
<div onScroll={handleScroll} style={{ height: '400px', overflowY: 'auto' }}>
{visibleNodes.map(([node, path]) => (
<div key={path.join('-')} style={{ height: '50px' }}>
{Node.string(node)}
</div>
))}
</div>
);
};Debounce expensive operations to improve performance:
import React, { useMemo } from 'react';
import { useSlateSelector } from 'slate-react';
import { debounce } from 'lodash';
const DebouncedAnalysis = () => {
// Debounced expensive analysis
const debouncedAnalysis = useMemo(
() => debounce((editor: Editor) => {
// Expensive analysis operation
return performComplexAnalysis(editor);
}, 300),
[]
);
const analysisResult = useSlateSelector((editor) => {
debouncedAnalysis(editor);
return 'Analysis in progress...';
});
return <div>{analysisResult}</div>;
};Load editor features on demand to reduce initial bundle size:
import React, { lazy, Suspense, useState } from 'react';
import { Editable } from 'slate-react';
// Lazy load heavy features
const AdvancedToolbar = lazy(() => import('./AdvancedToolbar'));
const SpellChecker = lazy(() => import('./SpellChecker'));
const ImageUploader = lazy(() => import('./ImageUploader'));
const LazyEditor = () => {
const [featuresEnabled, setFeaturesEnabled] = useState({
toolbar: false,
spellCheck: false,
imageUpload: false
});
return (
<div>
<button onClick={() => setFeaturesEnabled(prev => ({ ...prev, toolbar: !prev.toolbar }))}>
Toggle Toolbar
</button>
<Suspense fallback={<div>Loading...</div>}>
{featuresEnabled.toolbar && <AdvancedToolbar />}
{featuresEnabled.spellCheck && <SpellChecker />}
{featuresEnabled.imageUpload && <ImageUploader />}
</Suspense>
<Editable />
</div>
);
};// ✅ Good - specific selector
const wordCount = useSlateSelector(editor =>
Node.string(editor).split(/\s+/).length
);
// ❌ Bad - selecting entire editor
const editor = useSlate(); // Re-renders on every change
const wordCount = Node.string(editor).split(/\s+/).length;// ✅ Optimized component
const OptimizedComponent = memo(({ element }) => {
const expensiveValue = useMemo(() =>
performExpensiveCalculation(element),
[element.key]
);
const handleClick = useCallback(() => {
// Handle click
}, []);
return <div onClick={handleClick}>{expensiveValue}</div>;
});// ✅ Good performance
const ToolbarButton = () => {
const editor = useSlateStatic(); // No re-renders
const handleClick = useCallback(() => {
Editor.addMark(editor, 'bold', true);
}, [editor]);
return <button onClick={handleClick}>Bold</button>;
};
// ❌ Poor performance
const BadToolbarButton = () => {
const editor = useSlate(); // Re-renders on every editor change
return (
<button onClick={() => Editor.addMark(editor, 'bold', true)}>
Bold
</button>
);
};Install with Tessl CLI
npx tessl i tessl/npm-slate-react