refactor(renderer): spell out logical assignments React Compiler can't lower yet (#39339)

React Compiler skips any function containing `??=`, `||=` or `&&=`. Rewrite
the statement-position ones whose target re-reads without side effects
(identifiers and plain member chains) as the equivalent `if`:

  a ??= b  ->  if (a == null) { a = b }
  a ||= b  ->  if (!a) { a = b }
  a &&= b  ->  if (a) { a = b }

21 files; 25 fewer bailouts and 12 more compiled functions in the
compiler-compat baseline.

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-07 02:59:48 -08:00
committed by GitHub
co-authored by OrcaWin
parent cb3f367b33
commit 07e55b91f7
22 changed files with 98 additions and 60 deletions
+9 -21
View File
@@ -77,10 +77,8 @@
1 src/renderer/src/components/crash-report/CrashReportDialog.tsx
1 src/renderer/src/components/crash-report/CrashReportDialogSurface.tsx
3 src/renderer/src/components/crash-report/use-crash-report-copy.ts
1 src/renderer/src/components/dashboard-popout/useDashboardSnapshot.ts
2 src/renderer/src/components/dashboard/useAgentBucketCounts.ts
1 src/renderer/src/components/dashboard/useAgentBucketCounts.ts
1 src/renderer/src/components/dashboard/useLiveDashboardSnapshot.ts
1 src/renderer/src/components/dashboard/useRetainedAgents.ts
2 src/renderer/src/components/diff-comments/DiffCommentCard.tsx
5 src/renderer/src/components/diff-comments/DiffCommentPopover.tsx
2 src/renderer/src/components/diff-comments/diff-comment-draft-zone.ts
@@ -137,7 +135,6 @@
1 src/renderer/src/components/editor/rich-markdown/review/useRichMarkdownReviewData.ts
1 src/renderer/src/components/editor/rich-markdown/review/useRichMarkdownReviewRailController.ts
12 src/renderer/src/components/editor/rich-markdown/tables/RichMarkdownTableControls.tsx
2 src/renderer/src/components/editor/rich-markdown/tables/use-rich-markdown-table-control-target.ts
2 src/renderer/src/components/editor/use-editor-group-file-drop-owner.ts
1 src/renderer/src/components/editor/use-monaco-content-sync-bridge.ts
1 src/renderer/src/components/editor/use-monaco-editor-mount.ts
@@ -165,8 +162,7 @@
1 src/renderer/src/components/feature-wall/WorkbenchTerminalSplitMenu.tsx
1 src/renderer/src/components/feature-wall/agents-orchestration/OrchestrationPage.tsx
2 src/renderer/src/components/feature-wall/agents-orchestration/UsageAccountsCard.tsx
1 src/renderer/src/components/feature-wall/use-feature-wall-session-depth.ts
3 src/renderer/src/components/feature-wall/use-feature-wall-tour-telemetry.ts
2 src/renderer/src/components/feature-wall/use-feature-wall-tour-telemetry.ts
3 src/renderer/src/components/feature-wall/useAiCommitPrSettings.ts
1 src/renderer/src/components/floating-terminal/FloatingTerminalIconContextMenu.tsx
3 src/renderer/src/components/floating-terminal/FloatingTerminalToggleButton.tsx
@@ -211,9 +207,8 @@
2 src/renderer/src/components/mobile/use-mobile-page-paired-devices.ts
1 src/renderer/src/components/mobile/use-mobile-pairing-generation.ts
1 src/renderer/src/components/native-chat-restart-runtimes.ts
1 src/renderer/src/components/native-chat-resume-on-restart-store.ts
1 src/renderer/src/components/native-chat/AddToChatMenu.tsx
2 src/renderer/src/components/native-chat/NativeChatAccountUsageCard.tsx
1 src/renderer/src/components/native-chat/NativeChatAccountUsageCard.tsx
6 src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.tsx
1 src/renderer/src/components/native-chat/NativeChatComposerField.tsx
2 src/renderer/src/components/native-chat/NativeChatComposerToolbar.tsx
@@ -237,21 +232,19 @@
8 src/renderer/src/components/native-chat/use-native-chat-composer-attachments.ts
1 src/renderer/src/components/native-chat/use-native-chat-composer-keydown.ts
1 src/renderer/src/components/native-chat/use-native-chat-context-clear-floor.ts
3 src/renderer/src/components/native-chat/use-native-chat-find-matches.ts
1 src/renderer/src/components/native-chat/use-native-chat-find.ts
6 src/renderer/src/components/native-chat/use-native-chat-fork.ts
1 src/renderer/src/components/native-chat/use-native-chat-hand-off.tsx
1 src/renderer/src/components/native-chat/use-native-chat-history-retention.ts
1 src/renderer/src/components/native-chat/use-native-chat-launch-prompt-delivery-notice.ts
3 src/renderer/src/components/native-chat/use-native-chat-live-session.ts
4 src/renderer/src/components/native-chat/use-native-chat-message-rail.ts
2 src/renderer/src/components/native-chat/use-native-chat-live-session.ts
3 src/renderer/src/components/native-chat/use-native-chat-message-rail.ts
1 src/renderer/src/components/native-chat/use-native-chat-mobile-hold-notice.ts
2 src/renderer/src/components/native-chat/use-native-chat-paste-lifetime.ts
1 src/renderer/src/components/native-chat/use-native-chat-pending-delivery.ts
2 src/renderer/src/components/native-chat/use-native-chat-provisional-launch.ts
2 src/renderer/src/components/native-chat/use-native-chat-resolved-path-attachments.ts
4 src/renderer/src/components/native-chat/use-native-chat-retained-session.ts
2 src/renderer/src/components/native-chat/use-native-chat-selected-history.ts
3 src/renderer/src/components/native-chat/use-native-chat-retained-session.ts
1 src/renderer/src/components/native-chat/use-native-chat-selected-history.ts
1 src/renderer/src/components/native-chat/use-native-chat-session-context-actions.tsx
1 src/renderer/src/components/native-chat/use-native-chat-session-option-command.ts
2 src/renderer/src/components/native-chat/use-native-chat-skills.ts
@@ -278,7 +271,6 @@
2 src/renderer/src/components/onboarding/AgentStep.tsx
2 src/renderer/src/components/onboarding/NotificationStep.tsx
8 src/renderer/src/components/onboarding/OnboardingFlow.tsx
1 src/renderer/src/components/onboarding/OnboardingInlineCommandTerminal.tsx
3 src/renderer/src/components/onboarding/OnboardingLanguageSwitcher.tsx
1 src/renderer/src/components/onboarding/ThemeLookCarousel.tsx
1 src/renderer/src/components/onboarding/ThemeLookGallery.tsx
@@ -295,7 +287,6 @@
3 src/renderer/src/components/plugins/dialogs/PluginMarketplaceSourceDialog.tsx
1 src/renderer/src/components/plugins/frames/use-plugin-frame-entry.ts
1 src/renderer/src/components/plugins/frames/use-plugin-frame-host.ts
1 src/renderer/src/components/plugins/frames/use-plugin-panel-theme-revision.ts
4 src/renderer/src/components/plugins/list/PluginsSettingsSection.tsx
1 src/renderer/src/components/plugins/page/PluginContributionActionCommand.tsx
2 src/renderer/src/components/plugins/state/use-plugin-catalog.ts
@@ -303,7 +294,7 @@
1 src/renderer/src/components/plugins/state/use-plugin-install-action.ts
2 src/renderer/src/components/plugins/state/use-plugin-marketplace-lifecycle.ts
1 src/renderer/src/components/plugins/state/use-plugin-settings-writes.ts
3 src/renderer/src/components/ports/WorkspacePortScanner.tsx
1 src/renderer/src/components/ports/WorkspacePortScanner.tsx
2 src/renderer/src/components/provider-accounts/AntigravityChatAuthForm.tsx
1 src/renderer/src/components/pull-request-page/checks/tab.tsx
2 src/renderer/src/components/pull-request-page/comments/composer.tsx
@@ -360,7 +351,6 @@
1 src/renderer/src/components/right-sidebar/hosted-review/provider-actions/HostedReviewActions.tsx
6 src/renderer/src/components/right-sidebar/hosted-review/provider-actions/use-hosted-review-actions.ts
2 src/renderer/src/components/right-sidebar/hosted-review/provider-actions/use-ready-hosted-review-action.ts
1 src/renderer/src/components/right-sidebar/plugin-panels/declarative/use-plugin-declarative-tree.ts
1 src/renderer/src/components/right-sidebar/source-control/ai/text-generation-dialog-form.tsx
1 src/renderer/src/components/right-sidebar/source-control/ai/text-generation-dialog.tsx
1 src/renderer/src/components/right-sidebar/source-control/ai/use-conflict-recovery-composer.ts
@@ -389,7 +379,6 @@
1 src/renderer/src/components/right-sidebar/source-control/review/use-hosted-review-provider-hint.ts
1 src/renderer/src/components/right-sidebar/source-control/sync/git-history-panel.tsx
1 src/renderer/src/components/right-sidebar/source-control/sync/git-status-file-watch-refresh.ts
1 src/renderer/src/components/right-sidebar/source-control/sync/git-status-push-signal-refresh.ts
1 src/renderer/src/components/right-sidebar/source-control/sync/use-branch-compare.ts
1 src/renderer/src/components/right-sidebar/source-control/sync/use-conflict-abort.ts
1 src/renderer/src/components/right-sidebar/source-control/sync/use-remote-action-runner.ts
@@ -572,7 +561,7 @@
1 src/renderer/src/components/status-bar/RuntimeHostStatusRow.tsx
2 src/renderer/src/components/status-bar/SshTargetStatusRow.tsx
1 src/renderer/src/components/status-bar/icons.tsx
4 src/renderer/src/components/status-bar/status-bar-density.ts
3 src/renderer/src/components/status-bar/status-bar-density.ts
2 src/renderer/src/components/status-bar/use-codex-switcher-controller.ts
1 src/renderer/src/components/status-bar/use-owner-usage-rate-limits.ts
1 src/renderer/src/components/status-bar/use-provider-account-list.ts
@@ -687,7 +676,6 @@
2 src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.ts
1 src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-list.tsx
2 src/renderer/src/components/workspace-emoji/useWorkspaceEmojiShortcodeInput.ts
1 src/renderer/src/components/workspace-space/SpaceWindowStatePublisher.tsx
1 src/renderer/src/components/worktree-jump/use-worktree-jump-palette-list-entries.ts
1 src/renderer/src/components/worktree-jump/use-worktree-jump-palette-local-state.ts
1 src/renderer/src/components/worktree-jump/use-worktree-jump-palette-open-tabs.ts
@@ -50,7 +50,9 @@ export function useDashboardSnapshot(): DashboardSnapshot {
clearTimeout(topologyRefreshTimer)
}
const now = Date.now()
topologyRefreshStartedAt ??= now
if (topologyRefreshStartedAt == null) {
topologyRefreshStartedAt = now
}
const remainingMaxWait = Math.max(
0,
TOPOLOGY_REFRESH_MAX_WAIT_MS - (now - topologyRefreshStartedAt)
@@ -117,7 +117,9 @@ export function useAgentBucketCounts(): AgentBucketCounts {
const state = useAppStore(selectAgentBucketCountState)
// Cache per worktree so status/title churn in one workspace does not rebuild every row pipeline.
const cacheRef = useRef<ReturnType<typeof createDashboardBucketCountsCache>>(undefined!)
cacheRef.current ??= createDashboardBucketCountsCache()
if (cacheRef.current == null) {
cacheRef.current = createDashboardBucketCountsCache()
}
return useMemo(() => {
// Freshness follows agentStatusEpoch in state; read the clock when that generation invalidates the memo, not as a dependency.
// oxlint-disable-next-line react/purity -- wall time is sampled per epoch tick, as explained above.
@@ -138,7 +138,9 @@ export function useRetainedAgentsSync(): void {
TerminalTab,
TerminalTab
> | null>(null)
tabTopologyProjectionRef.current ??= createRetainedAgentsTabTopologyProjection()
if (tabTopologyProjectionRef.current == null) {
tabTopologyProjectionRef.current = createRetainedAgentsTabTopologyProjection()
}
const retainAgents = useAppStore((s) => s.retainAgents)
const pruneRetainedAgents = useAppStore((s) => s.pruneRetainedAgents)
const clearRetentionSuppressedPaneKeys = useAppStore((s) => s.clearRetentionSuppressedPaneKeys)
@@ -97,7 +97,9 @@ export function useRichMarkdownTableControlTarget(
if (cell && editorDom.contains(cell)) {
activate(cell)
pendingPointerRef.current = { cell, x: event.clientX, y: event.clientY }
pointerFrameRef.current ??= window.requestAnimationFrame(flushPointer)
if (pointerFrameRef.current == null) {
pointerFrameRef.current = window.requestAnimationFrame(flushPointer)
}
return
}
if (
@@ -133,10 +135,12 @@ export function useRichMarkdownTableControlTarget(
}
let layoutFrame: number | null = null
const update = (): void => {
layoutFrame ??= window.requestAnimationFrame(() => {
layoutFrame = null
setLayoutVersion((version) => version + 1)
})
if (layoutFrame == null) {
layoutFrame = window.requestAnimationFrame(() => {
layoutFrame = null
setLayoutVersion((version) => version + 1)
})
}
}
const observer = new ResizeObserver(update)
observer.observe(table)
@@ -48,12 +48,14 @@ export function useFeatureWallSessionDepth(
visitedReviewSteps: Set<ReviewStepId>
lastGroupId: FeatureWallWorkflowId | null
}>(undefined!)
sessionDepthRef.current ??= {
visitedWorkflows: new Set(),
visitedAgentSteps: new Set(),
visitedWorkbenchSteps: new Set(),
visitedReviewSteps: new Set(),
lastGroupId: null
if (sessionDepthRef.current == null) {
sessionDepthRef.current = {
visitedWorkflows: new Set(),
visitedAgentSteps: new Set(),
visitedWorkbenchSteps: new Set(),
visitedReviewSteps: new Set(),
lastGroupId: null
}
}
const getTourDepthSummary = useCallback((): FeatureWallTourDepthSummary => {
@@ -78,7 +78,9 @@ export function useFeatureWallTourTelemetry(args: {
}): { markExitAction: (exitAction: FeatureWallExitAction) => void } {
const { isOpen, source, getDepthSummary } = args
const telemetryRef = useRef<FeatureWallTourTelemetryState>(undefined!)
telemetryRef.current ??= createFeatureWallTourTelemetryState()
if (telemetryRef.current == null) {
telemetryRef.current = createFeatureWallTourTelemetryState()
}
const sourceRef = useRef(source)
const getDepthSummaryRef = useRef(getDepthSummary)
// Refresh payload refs before open/close effects and unmount cleanup can emit.
@@ -26,7 +26,9 @@ export function useNativeChatRestartOffer(enabled: boolean, store = localNativeC
const settingsLoaded = useAppStore((state) => state.settings !== null)
useEffect(() => {
if (enabled && settingsLoaded && store.target.kind === 'local') {
launch ??= loadLaunchOffer(store).finally(markNativeChatLaunchResumeDecided)
if (launch == null) {
launch = loadLaunchOffer(store).finally(markNativeChatLaunchResumeDecided)
}
}
}, [enabled, settingsLoaded, store])
return useSyncExternalStore(
@@ -186,9 +186,11 @@ function ChatUsageReset({
if (busy || !result.reset) {
return
}
attempt.current ??= {
idempotencyKey: createBrowserUuid(),
expectedScope: result.reset.expectedScope
if (attempt.current == null) {
attempt.current = {
idempotencyKey: createBrowserUuid(),
expectedScope: result.reset.expectedScope
}
}
setBusy(true)
setFailed(false)
@@ -220,15 +220,21 @@ export function useNativeChatFindMatches({
Array.from(record.removedNodes).some((node) => holdsMatch(node, matchNodesRef.current))
)
if (now) {
frame ??= requestAnimationFrame(runSearch)
if (frame == null) {
frame = requestAnimationFrame(runSearch)
}
} else {
timer ??= setTimeout(runSearch, TEXT_GROWTH_SEARCH_DELAY_MS)
if (timer == null) {
timer = setTimeout(runSearch, TEXT_GROWTH_SEARCH_DELAY_MS)
}
}
})
// A disclosure opens with a height animation (not a mutation): what it cut off at first shows at its end.
const onAnimationEnd = (event: Event): void => {
if (event.target instanceof Node && column?.contains(event.target)) {
frame ??= requestAnimationFrame(runSearch)
if (frame == null) {
frame = requestAnimationFrame(runSearch)
}
}
}
root.addEventListener('animationend', onAnimationEnd)
@@ -53,7 +53,9 @@ export function useNativeChatHistoryRetention({
const leaveEnd = useCallback(() => {
cancelTrim()
transition('browsing')
release.current ??= retainHistory?.() ?? null
if (release.current == null) {
release.current = retainHistory?.() ?? null
}
}, [retainHistory, cancelTrim, transition])
const returnToEnd = useCallback(
(reason?: 'send') => {
@@ -71,7 +71,9 @@ export function useNativeChatLiveSession(
const [appended, setAppended] = useState<NativeChatMessage[]>([])
// Cache the id index so each frame costs O(incoming), not O(existing).
const appendMergerRef = useRef<ReturnType<typeof createNativeChatMerger>>(undefined!)
appendMergerRef.current ??= createNativeChatMerger(NATIVE_CHAT_SOURCE_PRIORITY)
if (appendMergerRef.current == null) {
appendMergerRef.current = createNativeChatMerger(NATIVE_CHAT_SOURCE_PRIORITY)
}
const [hookState, hookStateStartedAt, hookHasWorkingSubagents] = useNativeChatHookStatus(paneKey)
@@ -92,7 +92,9 @@ export function useNativeChatMessageRail({
const slotIndex = previousItemsRef.current.find((item) => item.id === clicked.id)?.slotIndex
const row = virtualItemsRef.current.find((item) => item.index === slotIndex)
if (row && row.end > top && row.start < top + element.clientHeight) {
clicked.offset ??= row.start - top
if (clicked.offset == null) {
clicked.offset = row.start - top
}
if (Math.abs(row.start - top - clicked.offset) <= ALIGN_ARRIVED_PX) {
return
}
@@ -25,7 +25,9 @@ export function useNativeChatRetainedSession(
])
const activeIdentityRef = useRef(identity)
const retentionRef = useRef<ReturnType<typeof createNativeChatTranscriptRetention>>(undefined!)
retentionRef.current ??= createNativeChatTranscriptRetention()
if (retentionRef.current == null) {
retentionRef.current = createNativeChatTranscriptRetention()
}
const sessionMatchesIdentity = activeIdentityRef.current === identity
const readPhase = sessionMatchesIdentity ? session.readPhase : 'loading'
@@ -34,7 +34,9 @@ export function useNativeChatSelectedHistory({
return false
}
setSelectedSource(sourceKey)
release.current ??= retainHistory?.() ?? null
if (release.current == null) {
release.current = retainHistory?.() ?? null
}
return true
}, [root, retainHistory, sourceKey])
const deferTrim = useCallback(() => {
@@ -256,7 +256,9 @@ export function OnboardingInlineCommandTerminal({
return
}
if (hasPty) {
ptyFirstSeenAt ??= Date.now()
if (ptyFirstSeenAt == null) {
ptyFirstSeenAt = Date.now()
}
// GPU/canvas renderers may expose no DOM prompt text; settled PTY presence provides a bounded fallback.
if (Date.now() - ptyFirstSeenAt >= PTY_TEXT_FALLBACK_MS) {
scheduleInsert()
@@ -15,7 +15,9 @@ export function usePluginPanelThemeRevision(): number {
// Compare baked shell values: unrelated root-style writes must not remount iframes and destroy panel state.
const snapshotRef = useRef<string | null>(null)
useEffect(() => {
snapshotRef.current ??= readPanelThemeSnapshot()
if (snapshotRef.current == null) {
snapshotRef.current = readPanelThemeSnapshot()
}
const observer = new MutationObserver(() => {
const snapshot = readPanelThemeSnapshot()
if (snapshot === snapshotRef.current) {
@@ -148,7 +148,9 @@ export function WorkspacePortScanner({ enabled = true }: { enabled?: boolean }):
let sourceChanged =
Object.keys(scansByKey).length !== Object.keys(publishedScans).length
for (const { key, result } of reconciled) {
sourceChanged ||= scansByKey[key] !== result
if (!sourceChanged) {
sourceChanged = scansByKey[key] !== result
}
scansByKey[key] = result
}
const merged = mergeWorkspacePortScans(scansByKey)
@@ -290,7 +292,9 @@ export function WorkspacePortScanner({ enabled = true }: { enabled?: boolean }):
return
}
// Keep the leading scan through the quiet window so sequential events share it too.
burstRefresh ??= refresh({ force: true, targets: [LOCAL_TARGET] })
if (burstRefresh == null) {
burstRefresh = refresh({ force: true, targets: [LOCAL_TARGET] })
}
void burstRefresh.finally(() => {
if (disposed || sequence !== eventSequence) {
return
@@ -144,7 +144,9 @@ export function usePluginDeclarativeTree(
return {
onRender: (payload) => {
pendingRef.current = { payload }
frameRef.current ??= requestAnimationFrame(flush)
if (frameRef.current == null) {
frameRef.current = requestAnimationFrame(flush)
}
},
onRefused: (message, retryAfterMs) => {
const payload: unknown =
@@ -101,7 +101,9 @@ export function useGitStatusPushSignalRefresh({
fetchStatus
}
observers.add(observer)
disposeSignals ??= subscribeSignals()
if (disposeSignals == null) {
disposeSignals = subscribeSignals()
}
return () => {
observers.delete(observer)
if (observers.size === 0) {
@@ -152,7 +152,9 @@ export function useStatusBarDensity(): {
const bind =
(target: ElementRef) =>
(node: HTMLElement | null): void => {
observerRef.current ??= new ResizeObserver(() => evaluate())
if (observerRef.current == null) {
observerRef.current = new ResizeObserver(() => evaluate())
}
if (target.current) {
observerRef.current.unobserve(target.current)
}
@@ -41,11 +41,13 @@ export function SpaceWindowStatePublisher(): null {
}
const offRequest = api.onWindowStateRequested(() => {
windowOpen = true
offStore ??= useAppStore.subscribe((next, previous) => {
if (spaceWindowStateChanged(previous, next)) {
queuePublish()
}
})
if (offStore == null) {
offStore = useAppStore.subscribe((next, previous) => {
if (spaceWindowStateChanged(previous, next)) {
queuePublish()
}
})
}
publish(true)
})
const offClose = api.onWindowClosed?.(() => {