Skip to content

Commit 915296b

Browse files
ryaneggzclaude
andcommitted
feat: convert state-sync effects to derived values, add useMountEffect to contexts (#922)
Phase 3-5 of useEffect refactoring: - ToolTimelineItem: derive isExpanded from initial message.artifact instead of effect - FileEditorPanel: derive isTreeCollapsed from isMobile, derive effectiveShowPreview instead of effect-based reset, remove isRecording state sync (use prop directly), inline latestEditorValueRef assignment instead of effect - ChatContext: convert loadPersistentContextFiles mount effect to useMountEffect - AppContext: convert fetchAppVersion mount effect to useMountEffect Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: ryaneggz <kre8mymedia@gmail.com>
1 parent bc15cb3 commit 915296b

4 files changed

Lines changed: 32 additions & 52 deletions

File tree

frontend/src/components/panels/FileEditorPanel.tsx

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -152,17 +152,9 @@ export default function FileEditorPanel() {
152152

153153
// Tree sidebar state - auto-collapse on mobile
154154
const isMobile = useIsMobile();
155-
const [isTreeCollapsed, setIsTreeCollapsed] = useState(false);
155+
const [isTreeCollapsed, setIsTreeCollapsed] = useState(isMobile);
156156

157-
// Auto-collapse tree sidebar on mobile
158-
useEffect(() => {
159-
if (isMobile) {
160-
setIsTreeCollapsed(true);
161-
}
162-
}, [isMobile]);
163-
164-
// Voice recording state
165-
const [isRecording, setIsRecording] = useState(false);
157+
// Voice recording state (derived from recorderControls below)
166158
const recorderControls = useVoiceVisualizer();
167159
const { startRecording, stopRecording, isRecordingInProgress, recordedBlob } =
168160
recorderControls;
@@ -222,14 +214,10 @@ export default function FileEditorPanel() {
222214
// Use activeFile from context (no local selectedFile state needed)
223215
const selectedFile = activeFile;
224216

225-
useEffect(() => {
226-
if (!selectedFile) {
227-
latestEditorValueRef.current = "";
228-
return;
229-
}
230-
231-
latestEditorValueRef.current = getFileContent(selectedFile);
232-
}, [getFileContent, selectedFile]);
217+
// Keep editor value ref in sync (ref assignment, not state)
218+
latestEditorValueRef.current = selectedFile
219+
? getFileContent(selectedFile)
220+
: "";
233221

234222
// Parse selected file path into breadcrumb segments
235223
const breadcrumbSegments = useMemo((): BreadcrumbSegment[] => {
@@ -244,22 +232,10 @@ export default function FileEditorPanel() {
244232
}));
245233
}, [selectedFile]);
246234

247-
// Reset preview when switching to non-previewable file
248-
useEffect(() => {
249-
if (
250-
selectedFile &&
251-
!isMarkdownFile(selectedFile) &&
252-
!isHtmlFile(selectedFile) &&
253-
!isMermaidFile(selectedFile)
254-
) {
255-
setShowPreview(false);
256-
}
257-
}, [selectedFile]);
235+
// effectiveShowPreview is computed below, after helper function definitions
258236

259-
// Track recording state changes
260-
useEffect(() => {
261-
setIsRecording(isRecordingInProgress);
262-
}, [isRecordingInProgress]);
237+
// Use isRecordingInProgress directly instead of syncing to local state
238+
const isRecording = isRecordingInProgress;
263239

264240
// Handle recorded blob - transcribe and optionally send to LLM for inference
265241
useEffect(() => {
@@ -487,6 +463,14 @@ export default function FileEditorPanel() {
487463
return filename.toLowerCase().endsWith(".mmd");
488464
};
489465

466+
// Derive effective preview state — disable for non-previewable files
467+
const canPreview =
468+
!!selectedFile &&
469+
(isMarkdownFile(selectedFile) ||
470+
isHtmlFile(selectedFile) ||
471+
isMermaidFile(selectedFile));
472+
const effectiveShowPreview = showPreview && canPreview;
473+
490474
// Validate file path
491475
const validatePath = (path: string, excludePath?: string): string => {
492476
if (!path.trim()) return "Path is required";
@@ -1114,17 +1098,17 @@ export default function FileEditorPanel() {
11141098
isHtmlFile(selectedFile) ||
11151099
isMermaidFile(selectedFile)) && (
11161100
<Button
1117-
variant={showPreview ? "secondary" : "ghost"}
1101+
variant={effectiveShowPreview ? "secondary" : "ghost"}
11181102
size="sm"
11191103
onClick={() => setShowPreview(!showPreview)}
11201104
className="h-8 gap-2"
11211105
title={
1122-
showPreview
1106+
effectiveShowPreview
11231107
? "Show code"
11241108
: `Preview ${isHtmlFile(selectedFile) ? "HTML" : isMermaidFile(selectedFile) ? "Mermaid diagram" : "markdown"}`
11251109
}
11261110
aria-label={
1127-
showPreview
1111+
effectiveShowPreview
11281112
? "Show code"
11291113
: `Preview ${isHtmlFile(selectedFile) ? "HTML" : isMermaidFile(selectedFile) ? "Mermaid diagram" : "markdown"}`
11301114
}
@@ -1208,20 +1192,20 @@ export default function FileEditorPanel() {
12081192
<div className="flex-1 overflow-hidden">
12091193
{selectedFile && fileSystem.has(selectedFile) ? (
12101194
<>
1211-
{showPreview && isMarkdownFile(selectedFile) ? (
1195+
{effectiveShowPreview && isMarkdownFile(selectedFile) ? (
12121196
<ScrollArea className="h-full">
12131197
<div className="p-6 max-w-4xl mx-auto">
12141198
<MarkdownCard content={getFileContent(selectedFile)} />
12151199
</div>
12161200
</ScrollArea>
1217-
) : showPreview && isHtmlFile(selectedFile) ? (
1201+
) : effectiveShowPreview && isHtmlFile(selectedFile) ? (
12181202
<iframe
12191203
srcDoc={getFileContent(selectedFile)}
12201204
sandbox="allow-same-origin"
12211205
className="w-full h-full border-0 bg-white"
12221206
title={`Preview of ${selectedFile}`}
12231207
/>
1224-
) : showPreview && isMermaidFile(selectedFile) ? (
1208+
) : effectiveShowPreview && isMermaidFile(selectedFile) ? (
12251209
<ScrollArea className="h-full">
12261210
<div className="p-6 max-w-4xl mx-auto">
12271211
<MarkdownCard

frontend/src/components/timeline/ToolTimelineItem.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, lazy, Suspense, useEffect } from "react";
1+
import { useState, lazy, Suspense } from "react";
22
import { ChevronDown, ChevronUp, Wrench } from "lucide-react";
33
import { cn } from "@/lib/utils";
44
import { truncateFrom } from "@/lib/utils/format";
@@ -104,7 +104,7 @@ export default function ToolTimelineItem({
104104
message,
105105
maxPreviewLength = 100,
106106
}: ToolTimelineItemProps) {
107-
const [isExpanded, setIsExpanded] = useState(false);
107+
const [isExpanded, setIsExpanded] = useState(!!message.artifact);
108108

109109
// Get preview text from content
110110
const getPreviewText = () => {
@@ -120,12 +120,6 @@ export default function ToolTimelineItem({
120120

121121
const isSuccess = message.status === "success";
122122

123-
useEffect(() => {
124-
if (message.artifact) {
125-
setIsExpanded(true);
126-
}
127-
}, [message.artifact]);
128-
129123
return (
130124
<div className="bg-muted/50 rounded-lg border border-border/50 overflow-hidden">
131125
{/* Header - always visible */}

frontend/src/context/AppContext.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { useContext, createContext, useEffect } from "react";
1+
import { useContext, createContext } from "react";
22
import useAppHook from "@/hooks/useAppHook";
33
import { useDocumentTitle } from "@/hooks/useDocumentTitle";
4+
import { useMountEffect } from "@/hooks/useMountEffect";
45

56
export const AppContext = createContext({});
67

@@ -17,9 +18,9 @@ export default function AppProvider({
1718
}) {
1819
const appHooks = useAppHook();
1920

20-
useEffect(() => {
21+
useMountEffect(() => {
2122
appHooks.fetchAppVersion();
22-
}, []);
23+
});
2324

2425
return (
2526
<AppContext.Provider

frontend/src/context/ChatContext.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import useThread from "@/hooks/useThread";
1414
import useModel from "@/hooks/useModel";
1515
import useFileSystem, { type FileData } from "@/hooks/useFileSystem";
1616
import useMessageQueue from "@/hooks/useMessageQueue";
17+
import { useMountEffect } from "@/hooks/useMountEffect";
1718
import MemoryService from "@/lib/services/memoryService";
1819
import {
1920
getSettings,
@@ -531,9 +532,9 @@ export default function ChatProvider({
531532
}
532533
}, [isAuthenticated]);
533534

534-
useEffect(() => {
535+
useMountEffect(() => {
535536
void loadPersistentContextFiles();
536-
}, [loadPersistentContextFiles]);
537+
});
537538

538539
useEffect(() => {
539540
const nextVisibleFiles = buildVisibleWorkspaceFiles({

0 commit comments

Comments
 (0)