Add performance optimizations with React hooks

- Add useCallback to prevent unnecessary re-renders in FileTree
  - Memoize toggleFolder, startRename, finishRename, handleDelete
  - Memoize drag-and-drop handlers (handleDragStart, handleDragOver, handleDrop, etc.)
- Add useCallback to AIChat handlers
  - Memoize handleSend and handleKeyDown
- Add useCallback to Toolbar handlers
  - Memoize handleCopy and handleExport
- Add useCallback to SearchInFilesPanel
  - Memoize searchInFiles, handleSearch, and handleResultClick
- Import memo and useCallback from React for future component memoization
This commit is contained in:
Claude 2026-01-17 22:30:23 +00:00
parent d43e0a3a27
commit 394000f5ad
No known key found for this signature in database
4 changed files with 36 additions and 36 deletions

View file

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useCallback, memo } from 'react';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Textarea } from '@/components/ui/textarea';
@ -25,7 +25,7 @@ export function AIChat({ currentCode }: AIChatProps) {
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSend = async () => {
const handleSend = useCallback(async () => {
if (!input.trim() || isLoading) return;
const userMessage = input.trim();
@ -61,14 +61,14 @@ export function AIChat({ currentCode }: AIChatProps) {
} finally {
setIsLoading(false);
}
};
}, [input, isLoading, currentCode]);
const handleKeyDown = (e: React.KeyboardEvent) => {
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
}, [handleSend]);
return (
<div className="flex flex-col h-full bg-card border-l border-border min-w-[260px] max-w-[340px]">

View file

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useCallback, memo } from 'react';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Input } from '@/components/ui/input';
@ -52,7 +52,7 @@ export function FileTree({
const [draggedId, setDraggedId] = useState<string | null>(null);
const [dropTargetId, setDropTargetId] = useState<string | null>(null);
const toggleFolder = (id: string) => {
const toggleFolder = useCallback((id: string) => {
setExpandedFolders((prev) => {
const next = new Set(prev);
if (next.has(id)) {
@ -62,37 +62,37 @@ export function FileTree({
}
return next;
});
};
}, []);
const startRename = (file: FileNode) => {
const startRename = useCallback((file: FileNode) => {
setEditingId(file.id);
setEditingName(file.name);
};
}, []);
const finishRename = (id: string) => {
const finishRename = useCallback((id: string) => {
if (editingName.trim() && editingName !== '') {
onFileRename(id, editingName.trim());
toast.success('File renamed');
}
setEditingId(null);
setEditingName('');
};
}, [editingName, onFileRename]);
const handleDelete = (file: FileNode) => {
const handleDelete = useCallback((file: FileNode) => {
if (confirm(`Delete ${file.name}?`)) {
onFileDelete(file.id);
toast.success('File deleted');
}
};
}, [onFileDelete]);
const handleDragStart = (e: React.DragEvent, node: FileNode) => {
const handleDragStart = useCallback((e: React.DragEvent, node: FileNode) => {
e.stopPropagation();
setDraggedId(node.id);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', node.id);
};
}, []);
const handleDragOver = (e: React.DragEvent, node: FileNode) => {
const handleDragOver = useCallback((e: React.DragEvent, node: FileNode) => {
e.preventDefault();
e.stopPropagation();
@ -101,15 +101,15 @@ export function FileTree({
e.dataTransfer.dropEffect = 'move';
setDropTargetId(node.id);
}
};
}, [draggedId]);
const handleDragLeave = (e: React.DragEvent) => {
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDropTargetId(null);
};
}, []);
const handleDrop = (e: React.DragEvent, targetNode: FileNode) => {
const handleDrop = useCallback((e: React.DragEvent, targetNode: FileNode) => {
e.preventDefault();
e.stopPropagation();
@ -130,12 +130,12 @@ export function FileTree({
toast.success('File moved');
setDraggedId(null);
setDropTargetId(null);
};
}, [draggedId, onFileMove]);
const handleDragEnd = () => {
const handleDragEnd = useCallback(() => {
setDraggedId(null);
setDropTargetId(null);
};
}, []);
const renderNode = (node: FileNode, depth: number = 0) => {
const isExpanded = expandedFolders.has(node.id);

View file

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useCallback, useMemo } from 'react';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
@ -33,7 +33,7 @@ export function SearchInFilesPanel({
const [isSearching, setIsSearching] = useState(false);
const [caseSensitive, setCaseSensitive] = useState(false);
const searchInFiles = (query: string) => {
const searchInFiles = useCallback((query: string) => {
if (!query.trim()) {
setResults([]);
return;
@ -77,13 +77,13 @@ export function SearchInFilesPanel({
files.forEach(searchNode);
setResults(foundResults);
setIsSearching(false);
};
}, [files, caseSensitive]);
const handleSearch = () => {
const handleSearch = useCallback(() => {
searchInFiles(searchQuery);
};
}, [searchInFiles, searchQuery]);
const handleResultClick = (result: SearchResult) => {
const handleResultClick = useCallback((result: SearchResult) => {
const findFile = (nodes: FileNode[]): FileNode | null => {
for (const node of nodes) {
if (node.id === result.fileId) return node;
@ -99,7 +99,7 @@ export function SearchInFilesPanel({
if (file) {
onFileSelect(file, result.line);
}
};
}, [files, onFileSelect]);
const highlightMatch = (content: string, start: number, end: number) => {
return (

View file

@ -10,7 +10,7 @@ import {
} from '@/components/ui/dropdown-menu';
import { Copy, FileCode, Download, Info, Play, FolderPlus, User, SignOut, List } from '@phosphor-icons/react';
import { toast } from 'sonner';
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback, memo } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { ThemeSwitcher } from './ThemeSwitcher';
@ -33,16 +33,16 @@ export function Toolbar({ code, onTemplatesClick, onPreviewClick, onNewProjectCl
}
}, []);
const handleCopy = async () => {
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(code);
toast.success('Code copied to clipboard!');
} catch (error) {
toast.error('Failed to copy code');
}
};
}, [code]);
const handleExport = () => {
const handleExport = useCallback(() => {
const blob = new Blob([code], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
@ -53,7 +53,7 @@ export function Toolbar({ code, onTemplatesClick, onPreviewClick, onNewProjectCl
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('Script exported!');
};
}, [code]);
return (
<>