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:
OrcaWin
2026-11-05 19:33:45 -08:00
committed by GitHub
co-authored by OrcaWin
parent 8f0ccf0016
commit eecd4dc6d5
12 changed files with 60 additions and 58 deletions
@@ -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
@@ -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
@@ -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 (