Skip to content

Commit f052aff

Browse files
committed
🛠️ [fix] Defer state updates in effects using named UI delay
- Replaces direct 0ms timeouts in effect hooks with a shared constant for deferred state updates, clarifying intent and improving maintainability - Updates documentation to explain use of deferred updates and modern React state patterns - Helps comply with React best practices by avoiding direct state changes in effects, reducing risk of unexpected behavior
1 parent 3394581 commit f052aff

8 files changed

Lines changed: 32 additions & 10 deletions

File tree

src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { setupCacheSync } from "./utils/cacheSync";
4747
* @remarks
4848
* This is the primary entry point component that orchestrates the entire application
4949
* including state management, theming, error handling, and real-time updates.
50+
* Uses deferred state updates via timeouts to comply with React best practices.
5051
*
5152
* @public
5253
*
@@ -105,7 +106,7 @@ function App() {
105106
useEffect((): (() => void) | undefined => {
106107
if (!isLoading) {
107108
// Use timeout to defer state update to avoid direct call in useEffect
108-
const clearTimeoutId = setTimeout(clearLoadingOverlay, 0);
109+
const clearTimeoutId = setTimeout(clearLoadingOverlay, UI_DELAYS.STATE_UPDATE_DEFER);
109110
return (): void => {
110111
clearTimeout(clearTimeoutId);
111112
};

src/components/AddSiteForm/AddSiteForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export const AddSiteForm = React.memo(function AddSiteForm() {
128128
useEffect(() => {
129129
if (!isLoading) {
130130
// Use timeout to defer state update to avoid direct call in useEffect
131-
const clearTimeoutId = setTimeout(clearButtonLoading, 0);
131+
const clearTimeoutId = setTimeout(clearButtonLoading, UI_DELAYS.STATE_UPDATE_DEFER);
132132
return () => clearTimeout(clearTimeoutId);
133133
}
134134

src/components/Settings/Settings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export function Settings({ onClose }: Readonly<SettingsProperties>) {
8686
useEffect(() => {
8787
if (!isLoading) {
8888
// Use timeout to defer state update to avoid direct call in useEffect
89-
const clearTimeoutId = setTimeout(clearButtonLoading, 0);
89+
const clearTimeoutId = setTimeout(clearButtonLoading, UI_DELAYS.STATE_UPDATE_DEFER);
9090
return () => clearTimeout(clearTimeoutId);
9191
}
9292

src/components/SiteDetails/ScreenshotThumbnail.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import React, { useCallback, useEffect, useRef, useState } from "react";
1010
import { createPortal } from "react-dom";
1111

12+
import { UI_DELAYS } from "../../constants";
1213
import logger from "../../services/logger";
1314
import { useTheme } from "../../theme/useTheme";
1415

@@ -60,7 +61,7 @@ export function ScreenshotThumbnail({ siteName, url }: ScreenshotThumbnailProper
6061
hoverTimeoutReference.current = undefined;
6162
}
6263
// Use timeout to defer state update to avoid direct call in useEffect
63-
const clearTimeoutId = setTimeout(clearOverlayVariables, 0);
64+
const clearTimeoutId = setTimeout(clearOverlayVariables, UI_DELAYS.STATE_UPDATE_DEFER);
6465
return () => clearTimeout(clearTimeoutId);
6566
}
6667
return () => {};
@@ -120,7 +121,7 @@ export function ScreenshotThumbnail({ siteName, url }: ScreenshotThumbnailProper
120121
useEffect(() => {
121122
if (hovered && linkReference.current) {
122123
// Use timeout to defer state update to avoid direct call in useEffect
123-
const updateTimeoutId = setTimeout(updateOverlayPosition, 0);
124+
const updateTimeoutId = setTimeout(updateOverlayPosition, UI_DELAYS.STATE_UPDATE_DEFER);
124125
return () => clearTimeout(updateTimeoutId);
125126
}
126127
return () => {};

src/components/SiteDetails/useAddSiteForm.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
/**
22
* Custom hook for managing add site form state and validation.
3+
*
34
* Provides comprehensive form state management for creating new sites and adding monitors to existing sites.
45
* Supports real-time validation, automatic UUID generation, and error handling.
6+
*
7+
* @remarks
8+
* This hook uses render-time state management with previous value tracking to handle
9+
* state resets when the selected monitor changes, following modern React patterns
10+
* that avoid direct setState calls in useEffect hooks.
511
*/
612

713
import { useCallback, useState } from "react";

src/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,13 @@ export const UI_DELAYS = {
240240
LOADING_BUTTON: 100, // Delay before showing loading spinners (ms)
241241
/** Delay before showing loading overlay in milliseconds */
242242
LOADING_OVERLAY: 100, // Delay before showing loading overlay (ms)
243+
/**
244+
* Minimal delay to defer state updates in useEffect cleanup.
245+
* @remarks
246+
* Used to comply with React best practices by avoiding direct setState calls
247+
* in useEffect. The 0ms delay defers execution to the next tick of the event loop.
248+
*/
249+
STATE_UPDATE_DEFER: 0, // Defers state updates to next event loop tick (ms)
243250
} as const;
244251

245252
/**

src/hooks/site/useSiteDetails.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
* - Site data and monitor information
66
* - UI state (active tab, loading states)
77
* - Site operations (start/stop monitoring, check now, update settings)
8-
* - Local state management for editable fields
8+
* - Derived state management for editable fields (computed during render)
99
* - Integration with analytics data
10+
*
11+
* @remarks
12+
* This hook uses modern React patterns with derived state computed during render
13+
* instead of managing state in useEffect hooks. Changes are tracked using previous
14+
* value comparison and user edit state to provide responsive UI feedback.
1015
*/
1116

1217
import { useCallback, useEffect, useState } from "react";

src/theme/useTheme.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { isSiteStatus, type MonitorStatus, type SiteStatus } from "@shared/types";
99
import { useCallback, useEffect, useState } from "react";
1010

11+
import { UI_DELAYS } from "../constants";
1112
import { useSettingsStore } from "../stores/settings/useSettingsStore";
1213
import { themeManager } from "./ThemeManager";
1314
import { Theme, ThemeName } from "./types";
@@ -154,24 +155,25 @@ export function useTheme() {
154155
// Update theme when settings or systemTheme change
155156
useEffect(() => {
156157
// Use timeout to defer state update to avoid direct call in useEffect
157-
const updateTimeoutId = setTimeout(updateCurrentTheme, 0);
158+
const updateTimeoutId = setTimeout(updateCurrentTheme, UI_DELAYS.STATE_UPDATE_DEFER);
158159
return () => clearTimeout(updateTimeoutId);
159160
}, [settings.theme, systemTheme, updateCurrentTheme]);
160161

161162
// Listen for system theme changes
162163
useEffect(() => {
163164
const timeoutIds: NodeJS.Timeout[] = [];
164-
165+
165166
const cleanup = themeManager.onSystemThemeChange((isDark) => {
166167
const newSystemTheme = isDark ? "dark" : "light";
167168
// Use timeout to defer state update to avoid direct call in useEffect
168-
const timeoutId = setTimeout(() => updateSystemTheme(newSystemTheme), 0);
169+
const timeoutId = setTimeout(() => updateSystemTheme(newSystemTheme), UI_DELAYS.STATE_UPDATE_DEFER);
169170
timeoutIds.push(timeoutId);
170171
});
171172

172173
// Set initial system theme using timeout
173174
const initialTheme = themeManager.getSystemThemePreference();
174-
const initialTimeoutId = setTimeout(() => updateSystemTheme(initialTheme), 0);
175+
// eslint-disable-next-line @eslint-react/web-api/no-leaked-timeout -- Timeout is properly cleaned up in the forEach loop below
176+
const initialTimeoutId = setTimeout(() => updateSystemTheme(initialTheme), UI_DELAYS.STATE_UPDATE_DEFER);
175177
timeoutIds.push(initialTimeoutId);
176178

177179
return () => {

0 commit comments

Comments
 (0)