mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
perf(renderer): derive state instead of syncing it in effects (surfaces) (#38797)
Replace setState-only effects with render-time adjustment (prev-value or self-limiting guard) so prop changes no longer commit a stale render and then re-render. Drop the now-obsolete no-derived-state-effect suppression for use-combined-diff-view-preferences.ts. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -86,7 +86,6 @@ const SUPPRESSED_REACT_DOCTOR_DIAGNOSTICS = new Map([
|
||||
[
|
||||
'react-doctor(no-derived-state-effect)',
|
||||
new Set([
|
||||
'src/renderer/src/components/editor/combined-diff/review-controls/use-combined-diff-view-preferences.ts',
|
||||
// The open-cycle reset effect predates this entry; it surfaced when its body changed.
|
||||
'src/renderer/src/components/right-sidebar/source-control/ai/useSourceControlAgentActionDialog.ts'
|
||||
])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ImeTextarea } from '@/lib/ime-text-field'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
@@ -59,20 +59,17 @@ export function BrowserPageAnnotationTray({
|
||||
const [editComment, setEditComment] = useState('')
|
||||
const [editIntent, setEditIntent] = useState<BrowserAnnotationIntent>('change')
|
||||
const editImages = useBrowserAnnotationImages(worktreeId)
|
||||
const resetEditImages = editImages.resetImages
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
const ToggleChevron = expanded ? ChevronDown : ChevronRight
|
||||
|
||||
// Why: a delete or clear while a row is mid-edit must not leave edit state pointing at nothing.
|
||||
useEffect(() => {
|
||||
if (
|
||||
editingAnnotationId &&
|
||||
!browserAnnotations.some((annotation) => annotation.id === editingAnnotationId)
|
||||
) {
|
||||
setEditingAnnotationId(null)
|
||||
resetEditImages()
|
||||
}
|
||||
}, [browserAnnotations, editingAnnotationId, resetEditImages])
|
||||
if (
|
||||
editingAnnotationId &&
|
||||
!browserAnnotations.some((annotation) => annotation.id === editingAnnotationId)
|
||||
) {
|
||||
setEditingAnnotationId(null)
|
||||
editImages.resetImages()
|
||||
}
|
||||
|
||||
const handleStartEdit = (annotation: BrowserPageAnnotation): void => {
|
||||
setEditingAnnotationId(annotation.id)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useLayoutEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { emitBrowserCookieImportToast } from '@/lib/browser-cookie-import-toast'
|
||||
import { useAppStore } from '@/store'
|
||||
@@ -80,7 +80,13 @@ export function BrowserToolbarMenu({
|
||||
const [pendingSwitchProfileId, setPendingSwitchProfileId] = useState<string | null | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
// Only expose the hidden Import Cookies row when the tour reaches its fallback step.
|
||||
const [menuOpen, setMenuOpen] = useState(shouldForceMenuOpen)
|
||||
const [prevShouldForceMenuOpen, setPrevShouldForceMenuOpen] = useState(shouldForceMenuOpen)
|
||||
if (prevShouldForceMenuOpen !== shouldForceMenuOpen) {
|
||||
setPrevShouldForceMenuOpen(shouldForceMenuOpen)
|
||||
setMenuOpen(shouldForceMenuOpen)
|
||||
}
|
||||
const [siteDisplay, setSiteDisplay] = useState<BrowserPageSiteDisplay | null>(null)
|
||||
const mountedRef = useMountedRef()
|
||||
|
||||
@@ -112,11 +118,6 @@ export function BrowserToolbarMenu({
|
||||
})
|
||||
}, [browserPageId, menuOpen, mountedRef])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// Only expose the hidden Import Cookies row when the tour reaches its fallback step.
|
||||
setMenuOpen(shouldForceMenuOpen)
|
||||
}, [shouldForceMenuOpen])
|
||||
|
||||
const handleMenuOpenChange = (open: boolean): void => {
|
||||
if (shouldForceMenuOpen && !open) {
|
||||
return
|
||||
|
||||
+11
-5
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import type React from 'react'
|
||||
import type { DiffSection } from '../../diff/sections/diff-section-types'
|
||||
import { combinedDiffViewPreferences } from '../remember-view/combined-diff-view-memory'
|
||||
@@ -44,20 +44,26 @@ export function useCombinedDiffViewPreferences({
|
||||
)
|
||||
|
||||
// Use settings until the first session toolbar choice, then retain that choice over global defaults.
|
||||
useEffect(() => {
|
||||
const [prevDiffDefaultView, setPrevDiffDefaultView] = useState(diffDefaultView)
|
||||
if (prevDiffDefaultView !== diffDefaultView) {
|
||||
setPrevDiffDefaultView(diffDefaultView)
|
||||
if (diffDefaultView !== undefined && combinedDiffViewPreferences.sideBySide === null) {
|
||||
setSideBySide(diffDefaultView === 'side-by-side')
|
||||
}
|
||||
}, [diffDefaultView])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const [prevFileTreeVisibleByDefault, setPrevFileTreeVisibleByDefault] = useState(
|
||||
combinedDiffFileTreeVisibleByDefault
|
||||
)
|
||||
if (prevFileTreeVisibleByDefault !== combinedDiffFileTreeVisibleByDefault) {
|
||||
setPrevFileTreeVisibleByDefault(combinedDiffFileTreeVisibleByDefault)
|
||||
if (
|
||||
combinedDiffFileTreeVisibleByDefault !== undefined &&
|
||||
combinedDiffViewPreferences.fileTreeCollapsed === null
|
||||
) {
|
||||
setFileTreeCollapsedState(combinedDiffFileTreeVisibleByDefault === false)
|
||||
}
|
||||
}, [combinedDiffFileTreeVisibleByDefault])
|
||||
}
|
||||
|
||||
const setFileTreeCollapsed = useCallback((collapsed: boolean) => {
|
||||
combinedDiffViewPreferences.fileTreeCollapsed = collapsed
|
||||
|
||||
+5
-2
@@ -53,9 +53,12 @@ export function MarkdownTableOfContentsPanel({
|
||||
setWidth: setMarkdownTocPanelWidth
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Prune on change, not in a derivation, so a heading that is retyped does not resurrect its collapse.
|
||||
const [prevItems, setPrevItems] = useState(items)
|
||||
if (prevItems !== items) {
|
||||
setPrevItems(items)
|
||||
setCollapsedIds((current) => pruneMarkdownTocCollapsedIds(current, items))
|
||||
}, [items])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
|
||||
@@ -85,6 +85,12 @@ export function EmulatorDeviceFrame({
|
||||
const wheelGestureRef = useRef<PendingWheelGesture | null>(null)
|
||||
const [streamError, setStreamError] = useState(false)
|
||||
const [streamSize, setStreamSize] = useState<StreamSize | null>(null)
|
||||
const [prevStream, setPrevStream] = useState({ previewUrl, streamKey })
|
||||
if (prevStream.previewUrl !== previewUrl || prevStream.streamKey !== streamKey) {
|
||||
setPrevStream({ previewUrl, streamKey })
|
||||
setStreamError(false)
|
||||
setStreamSize(null)
|
||||
}
|
||||
const canInteract = isLive && !loading && !streamError
|
||||
const scrcpyDeviceId = scrcpyDeviceIdFromStreamUrl(previewUrl)
|
||||
const controlStream = useEmulatorControlStream(wsUrl, canInteract, scrcpyDeviceId)
|
||||
@@ -98,11 +104,6 @@ export function EmulatorDeviceFrame({
|
||||
const { enableKeyboardCapture, handleBlur, handleKeyDown, handlePaste, keyboardCaptureActive } =
|
||||
useEmulatorScreenKeyboard({ ...controlStream, canInteract })
|
||||
|
||||
useEffect(() => {
|
||||
setStreamError(false)
|
||||
setStreamSize(null)
|
||||
}, [previewUrl, streamKey])
|
||||
|
||||
const mapEventToScreenPoint = useCallback(
|
||||
(event: ScreenCoordinateEvent): EmulatorScreenPoint | null =>
|
||||
mapClientPointToSimulatorScreen(
|
||||
|
||||
@@ -251,11 +251,9 @@ export function useEmulatorPaneSession({
|
||||
]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedUdid && configuredDefaultUdid) {
|
||||
setSelectedUdid(configuredDefaultUdid)
|
||||
}
|
||||
}, [configuredDefaultUdid, selectedUdid])
|
||||
if (!selectedUdid && configuredDefaultUdid) {
|
||||
setSelectedUdid(configuredDefaultUdid)
|
||||
}
|
||||
|
||||
const shutdown = useEmulatorPaneShutdown({
|
||||
loading,
|
||||
|
||||
@@ -140,10 +140,12 @@ export function NativeChatResolvedView({
|
||||
const [commandMarkers, setCommandMarkers] = useState<NativeChatCommandMarker[]>(() =>
|
||||
readCommandMarkerCache(commandMarkerScope)
|
||||
)
|
||||
useEffect(() => {
|
||||
const [prevCommandMarkerScope, setPrevCommandMarkerScope] = useState(commandMarkerScope)
|
||||
if (prevCommandMarkerScope !== commandMarkerScope) {
|
||||
setPrevCommandMarkerScope(commandMarkerScope)
|
||||
setCommandMarkers(readCommandMarkerCache(commandMarkerScope))
|
||||
setWorkingInterrupted(false)
|
||||
}, [commandMarkerScope])
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!paneLaunchPrompt || !shouldPruneLaunchPrompt(paneLaunchPrompt, session.messages)) {
|
||||
return
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type MouseEventHandler,
|
||||
@@ -180,13 +179,9 @@ export function useNativeChatContextMenu({
|
||||
const hasItems =
|
||||
hasEditItems || hasSessionItems || chatPane !== undefined || resolveOrcaSessionId !== undefined
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
setState((current) =>
|
||||
current.open ? { ...current, open: false, image: undefined } : current
|
||||
)
|
||||
}
|
||||
}, [enabled])
|
||||
if (!enabled && state.open) {
|
||||
setState({ ...state, open: false, image: undefined })
|
||||
}
|
||||
|
||||
const onContextMenuCapture = useCallback(
|
||||
(event: React.MouseEvent<HTMLElement>) => {
|
||||
|
||||
@@ -51,7 +51,11 @@ export function useNativeChatPendingDelivery(args: {
|
||||
() => releaseEmptyTranscriptClaims(cachedPending, sessionId),
|
||||
[cachedPending, sessionId]
|
||||
)
|
||||
useEffect(() => setPending(readPendingSendCache(scope)), [scope])
|
||||
const [prevScope, setPrevScope] = useState(scope)
|
||||
if (prevScope !== scope) {
|
||||
setPrevScope(scope)
|
||||
setPending(readPendingSendCache(scope))
|
||||
}
|
||||
const save = useCallback(
|
||||
(update: (entries: NativeChatPendingSend[]) => NativeChatPendingSend[]) => {
|
||||
const current = readPendingSendCache(scope)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useLayoutEffect, useState } from 'react'
|
||||
import { memo, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useAppStore } from '../../store'
|
||||
import { isProvenProcessExit } from '../../../../shared/terminal/pty/lifecycle/terminal-exit-cause'
|
||||
@@ -50,11 +50,9 @@ export const TerminalOverlaySlot = memo(function TerminalOverlaySlot({
|
||||
const [shouldMeasureHiddenStartup, setShouldMeasureHiddenStartup] = useState(
|
||||
() => useAppStore.getState().pendingStartupByTabId[terminalTabId] !== undefined
|
||||
)
|
||||
useLayoutEffect(() => {
|
||||
if (isVisible && shouldMeasureHiddenStartup) {
|
||||
setShouldMeasureHiddenStartup(false)
|
||||
}
|
||||
}, [isVisible, shouldMeasureHiddenStartup])
|
||||
if (isVisible && shouldMeasureHiddenStartup) {
|
||||
setShouldMeasureHiddenStartup(false)
|
||||
}
|
||||
|
||||
// Drop with the reveal in one render, or a later lease release re-runs resume and steals focus.
|
||||
const prewarm = isVisible || activityTerminalPortal !== null ? null : hiddenPrewarm
|
||||
|
||||
@@ -153,11 +153,9 @@ export function useComposerInitialTargetState(input: ComposerInitialTargetStateI
|
||||
[actionableHostIds, projectGroups, selectedProjectGroupId]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProjectGroupId && !selectedProjectGroup) {
|
||||
setSelectedProjectGroupId(null)
|
||||
}
|
||||
}, [selectedProjectGroup, selectedProjectGroupId])
|
||||
if (selectedProjectGroupId && !selectedProjectGroup) {
|
||||
setSelectedProjectGroupId(null)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user