diff --git a/package-lock.json b/package-lock.json index 018dac1..2dd38e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@vitejs/plugin-react": "^6.0.1", "eslint": "^10.7.0", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", "globals": "^17.6.0", "identity-obj-proxy": "^3.0.0", @@ -1972,9 +1972,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1992,9 +1989,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2012,9 +2006,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2032,9 +2023,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2052,9 +2040,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2072,9 +2057,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3328,9 +3310,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3345,9 +3324,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3362,9 +3338,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3379,9 +3352,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3396,9 +3366,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3413,9 +3380,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3430,9 +3394,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3447,9 +3408,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3464,9 +3422,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3481,9 +3436,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4854,9 +4806,9 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", "dependencies": { @@ -4870,7 +4822,7 @@ "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-plugin-react-refresh": { diff --git a/package.json b/package.json index f6b47f6..54840f8 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@vitejs/plugin-react": "^6.0.1", "eslint": "^10.7.0", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", "globals": "^17.6.0", "identity-obj-proxy": "^3.0.0", diff --git a/src/providers/BackendContext.tsx b/src/providers/BackendContext.tsx index 6df888b..b0f52a6 100644 --- a/src/providers/BackendContext.tsx +++ b/src/providers/BackendContext.tsx @@ -78,6 +78,26 @@ function envUrls(): Partial> { return urls; } +// --------------------------------------------------------------------------- +// Pure helper — no setState, safe to call from effects via .then() +// --------------------------------------------------------------------------- + +async function fetchHealthStatus( + url: string +): Promise<{ connected: boolean; error: string | null }> { + try { + const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(5000) }); + return res.ok + ? { connected: true, error: null } + : { connected: false, error: `Health check returned ${res.status}` }; + } catch (err) { + return { + connected: false, + error: err instanceof Error ? err.message : 'Cannot reach backend', + }; + } +} + // --------------------------------------------------------------------------- // Provider component // --------------------------------------------------------------------------- @@ -100,7 +120,7 @@ export function BackendProvider({ children }: { children: ReactNode }) { backendId === 'local' || backendId === 'js' ? true : null ); const [connectionError, setConnectionError] = useState(null); - const [checking, setChecking] = useState(false); + const [checking, setChecking] = useState(backendId !== 'local' && backendId !== 'js'); // Build configs with URL overrides const backends = useMemo>(() => { @@ -121,7 +141,7 @@ export function BackendProvider({ children }: { children: ReactNode }) { return new RemoteProvider(backendId, url); }, [backendId, backends]); - // Health check for remote backends + // Health check for remote backends — called from event handlers (onClick), not effects const checkConnection = useCallback(async () => { if (backendId === 'local' || backendId === 'js') { setConnected(true); @@ -131,27 +151,32 @@ export function BackendProvider({ children }: { children: ReactNode }) { setChecking(true); const cfg = backends[backendId]; const url = cfg.url ?? BACKEND_PRESETS[backendId].url ?? ''; - try { - const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(5000) }); - if (res.ok) { - setConnected(true); - setConnectionError(null); - } else { - setConnected(false); - setConnectionError(`Health check returned ${res.status}`); - } - } catch (err) { - setConnected(false); - setConnectionError(err instanceof Error ? err.message : 'Cannot reach backend'); - } finally { - setChecking(false); - } + const result = await fetchHealthStatus(url); + setConnected(result.connected); + setConnectionError(result.error); + setChecking(false); }, [backendId, backends]); - // Auto-check when backend changes + // Auto-check when backend changes — setState only in .then() callback, never synchronously useEffect(() => { - checkConnection(); - }, [checkConnection]); + if (backendId === 'local' || backendId === 'js') return; + + let ignore = false; + const cfg = backends[backendId]; + const url = cfg.url ?? BACKEND_PRESETS[backendId].url ?? ''; + + fetchHealthStatus(url).then((result) => { + if (!ignore) { + setConnected(result.connected); + setConnectionError(result.error); + setChecking(false); + } + }); + + return () => { + ignore = true; + }; + }, [backendId, backends]); // Persist backend choice useEffect(() => { @@ -169,6 +194,7 @@ export function BackendProvider({ children }: { children: ReactNode }) { } setConnected(id === 'local' ? true : null); setConnectionError(null); + setChecking(id !== 'local' && id !== 'js'); setBackendId(id); }, []);