Instruction file imported from lobehub/lobe-editor (
.cursor/rules/react-development.mdc). Copyright stays with the author.
description: Comprehensive React component development guidelines for LobeHub Editor globs: .tsx,.jsx,src/react//,src/plugins//react//*
React Component Guidelines
Component Architecture
LobeHub Editor uses React components in two main contexts:
- High-level components in src/react/ - Main user-facing components
- Plugin components in src/plugins/*/react/ - Plugin-specific React implementations
Main Components
Primary components in src/react/:
- Editor - Core editor with plugin system
- ChatInput - Chat interface container
- ChatInputActions - Action management
- ChatInputActionBar - Action layout
- SendButton - Send functionality
- CodeLanguageSelect - Language selection
Component Development Pattern
Standard Component Definition
import { memo, useCallback, useState, ReactNode, CSSProperties } from 'react';
import { LexicalEditor } from 'lexical';
import { createDebugLogger } from '@/utils/debug';
export interface ComponentNameProps {
/** Required prop with JSDoc description */
requiredProp: string;
/** Optional prop with default value */
optionalProp?: boolean;
/** Event handler callback */
onEvent?: (data: EventData) => void;
/** Children for composition */
children?: ReactNode;
/** Style customization */
className?: string;
style?: CSSProperties;
}
export const ComponentName = memo<ComponentNameProps>(({
requiredProp,
optionalProp = false,
onEvent,
children,
className,
style,
...rest
}) => {
// Hooks and state
const [state, setState] = useState(initialValue);
const { styles, cx } = useStyles();
const logger = createDebugLogger('react', 'component-name');
// Event handlers with useCallback
const handleEvent = useCallback((data: EventData) => {
logger.debug('Event triggered:', data);
onEvent?.(data);
}, [onEvent, logger]);
// Render
return (
<div
className={cx(styles.container, className)}
style={style}
{...rest}
>
{children}
</div>
);
});
ComponentName.displayName = 'ComponentName';
Plugin Component Pattern
export interface ReactPluginNameProps {
/** Editor instance for command dispatch */
editor?: LexicalEditor;
/** Plugin configuration */
config?: PluginConfig;
/** Plugin state management */
state?: PluginState;
/** Plugin event callbacks */
onPluginEvent?: (event: PluginEvent) => void;
}
export const ReactPluginName = memo<ReactPluginNameProps>(({
editor,
config,
state,
onPluginEvent,
}) => {
const logger = createDebugLogger('plugin', 'plugin-name');
// Command dispatch to editor
const handleCommand = useCallback((command: Command, payload: any) => {
logger.debug('Dispatching command:', command, payload);
editor?.dispatchCommand(command, payload);
}, [editor, logger]);
// Plugin-specific event handling
const handlePluginEvent = useCallback((event: PluginEvent) => {
logger.debug('Plugin event:', event);
onPluginEvent?.(event);
}, [onPluginEvent, logger]);
return (
<div className="plugin-container">
{/* Plugin UI implementation */}
</div>
);
});
ReactPluginName.displayName = 'ReactPluginName';
Editor Integration Patterns
Main Editor Usage
import { Editor, useEditor } from '@lobehub/editor/react';
import {
ReactSlashPlugin,
ReactMentionPlugin,
ReactCodeblockPlugin
} from '@lobehub/editor';
function MyEditorComponent() {
const editor = useEditor();
const handleChange = useCallback((editor: IEditor) => {
const content = editor.getDocument('markdown');
// Handle content changes
}, []);
return (
<Editor
editorRef={editor}
placeholder="Start typing..."
plugins={[
ReactSlashPlugin,
ReactMentionPlugin,
ReactCodeblockPlugin,
]}
slashOption={{
items: slashCommands,
}}
mentionOption={{
items: mentionItems,
}}
onChange={handleChange}
/>
);
}
Chat Interface Pattern
import { ChatInput, ChatInputActionBar, ChatInputActions, SendButton } from '@lobehub/editor/react';
function ChatInterface() {
const handleSend = useCallback(() => {
// Handle send action
}, []);
return (
<ChatInput
header={<ChatHeader />}
footer={
<ChatInputActionBar
left={<ChatInputActions items={leftActions} />}
right={<SendButton onSend={handleSend} />}
/>
}
>
<Editor {...editorProps} />
</ChatInput>
);
}
Styling Standards
Style Hooks with antd-style
import { createStyles } from 'antd-style';
const useStyles = createStyles(({ css, token, isDarkMode }) => ({
container: css`
padding: ${token.padding}px;
border-radius: ${token.borderRadius}px;
background: ${isDarkMode ? token.colorBgElevated : token.colorBgContainer};
border: 1px solid ${token.colorBorder};
&:hover {
border-color: ${token.colorPrimary};
}
`,
active: css`
background: ${token.colorPrimary};
color: ${token.colorTextLightSolid};
`,
disabled: css`
opacity: 0.6;
cursor: not-allowed;
`,
}));
// Usage in component
const { styles, cx } = useStyles();
Event Handling
Command Integration
import { INSERT_HEADING_COMMAND, INSERT_LIST_COMMAND } from '@lobehub/editor';
const EditorToolbar = memo(({ editor }: { editor: LexicalEditor }) => {
const logger = createDebugLogger('react', 'toolbar');
const handleAction = useCallback((action: ActionType) => {
logger.debug('Toolbar action:', action);
switch (action.type) {
case 'heading':
editor.dispatchCommand(INSERT_HEADING_COMMAND, { tag: 'h1' });
break;
case 'list':
editor.dispatchCommand(INSERT_LIST_COMMAND, { listType: 'bullet' });
break;
}
}, [editor, logger]);
return <ActionButton onAction={handleAction} />;
});
State Management
const StatefulComponent = memo(() => {
// Local component state
const [isOpen, setIsOpen] = useState(false);
// Editor state integration
const editorState = useEditorState();
// Derived state with memoization
const canSave = useMemo(() => {
return editorState.hasContent && !editorState.isLoading;
}, [editorState.hasContent, editorState.isLoading]);
return (
<Button
disabled={!canSave}
onClick={() => setIsOpen(true)}
>
Save
</Button>
);
});
Performance Optimization
Memoization Best Practices
const OptimizedComponent = memo<Props>(({ items, onSelect, config }) => {
// Memoize expensive calculations
const processedItems = useMemo(() => {
return items.map(item => processItem(item, config));
}, [items, config]);
// Memoize event handlers
const handleSelect = useCallback((item: Item) => {
onSelect?.(item);
}, [onSelect]);
// Memoize derived values
const filteredItems = useMemo(() => {
return processedItems.filter(item => item.isVisible);
}, [processedItems]);
return (
<ItemList
items={filteredItems}
onSelect={handleSelect}
/>
);
});
Virtual Lists for Large Data
import { FixedSizeList as List } from 'react-window';
const VirtualizedList = memo<{ items: Item[] }>(({ items }) => {
const Row = useCallback(({ index, style }: ListChildComponentProps) => (
<div style={style}>
<ItemRenderer item={items[index]} />
</div>
), [items]);
return (
<List
height={400}
itemCount={items.length}
itemSize={50}
itemData={items}
>
{Row}
</List>
);
});
Demo Component Patterns
Development-Only Debugging
For demo components in demos/ directories:
import { devConsole } from '@/utils/debug';
const DemoComponent = memo(() => {
const handleDemoAction = useCallback((data: any) => {
// Use devConsole for demo-specific logging
devConsole.log('Demo action triggered:', data);
// Demo logic
}, []);
return <DemoUI onAction={handleDemoAction} />;
});
Component Testing
import { render, screen, fireEvent } from '@testing-library/react';
import { ComponentName } from './ComponentName';
describe('ComponentName', () => {
it('should render with required props', () => {
render(<ComponentName requiredProp="test" />);
expect(screen.getByText('test')).toBeInTheDocument();
});
it('should handle events correctly', () => {
const handleEvent = jest.fn();
render(<ComponentName onEvent={handleEvent} />);
fireEvent.click(screen.getByRole('button'));
expect(handleEvent).toHaveBeenCalledWith(expectedData);
});
it('should apply custom styles', () => {
render(<ComponentName className="custom-class" />);
expect(screen.getByRole('button')).toHaveClass('custom-class');
});
});
Best Practices
- Always use memo - Wrap components with
memofor performance - Proper prop types - Define comprehensive TypeScript interfaces
- Event handler memoization - Use
useCallbackfor event handlers - Expensive calculation memoization - Use
useMemofor heavy computations - Debug logging - Use appropriate debug loggers for different contexts
- Display names - Always set
displayNamefor debugging - Style isolation - Use antd-style's
createStylesfor component styling - Accessibility - Include proper ARIA attributes and keyboard navigation
- Error boundaries - Implement error boundaries for critical components
- Testing - Write comprehensive tests for component behavior