Chore: full react hooks eslint compliance (#7040)

This commit is contained in:
shamoon
2026-08-21 14:00:02 -07:00
committed by GitHub
parent 7c26cf6d97
commit 4c361422ca
111 changed files with 1072 additions and 867 deletions
-4
View File
@@ -30,10 +30,6 @@ export function ColorProvider({ initialTheme, children }) {
lastColor = rawColor;
};
useEffect(() => {
if (initialTheme !== undefined) setColor(initialTheme ?? getInitialColor());
}, [initialTheme]);
useEffect(() => {
rawSetColor(color);
}, [color]);
+1 -5
View File
@@ -1,14 +1,10 @@
import { createContext, useEffect, useMemo, useState } from "react";
import { createContext, useMemo, useState } from "react";
export const SettingsContext = createContext();
export function SettingsProvider({ initialSettings, children }) {
const [settings, setSettings] = useState(() => initialSettings ?? {});
useEffect(() => {
if (initialSettings !== undefined) setSettings(initialSettings ?? {});
}, [initialSettings]);
const value = useMemo(() => ({ settings, setSettings }), [settings]);
return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;
+1 -5
View File
@@ -1,14 +1,10 @@
import { createContext, useEffect, useMemo, useState } from "react";
import { createContext, useMemo, useState } from "react";
export const TabContext = createContext();
export function TabProvider({ initialTab, children }) {
const [activeTab, setActiveTab] = useState(() => initialTab ?? false);
useEffect(() => {
if (initialTab !== undefined) setActiveTab(initialTab ?? false);
}, [initialTab]);
const value = useMemo(() => ({ activeTab, setActiveTab }), [activeTab]);
return <TabContext.Provider value={value}>{children}</TabContext.Provider>;
-4
View File
@@ -31,10 +31,6 @@ export function ThemeProvider({ initialTheme, children }) {
localStorage.setItem("theme-mode", rawTheme);
};
useEffect(() => {
if (initialTheme !== undefined) setTheme(initialTheme ?? getInitialTheme());
}, [initialTheme]);
useEffect(() => {
rawSetTheme(theme);
}, [theme]);
+45
View File
@@ -0,0 +1,45 @@
import { useSyncExternalStore } from "react";
const MINUTE = 60_000;
const stores = new Map();
function getStore(refreshInterval) {
const existing = stores.get(refreshInterval);
if (existing) return existing;
const listeners = new Set();
let now = null; // null until mounted, so SSR and hydration render the same thing
let timer = null;
const store = {
getSnapshot: () => now,
subscribe: (onStoreChange) => {
listeners.add(onStoreChange);
if (!timer) {
now = Date.now();
timer = setInterval(() => {
now = Date.now();
listeners.forEach((listener) => listener());
}, refreshInterval);
}
return () => {
listeners.delete(onStoreChange);
if (!listeners.size) {
clearInterval(timer);
timer = null;
}
};
},
};
stores.set(refreshInterval, store);
return store;
}
// Returns null on the server and during hydration, then the current time.
export default function useCurrentTime(refreshInterval = MINUTE) {
const { subscribe, getSnapshot } = getStore(refreshInterval);
return useSyncExternalStore(subscribe, getSnapshot, () => null);
}
+32
View File
@@ -0,0 +1,32 @@
// @vitest-environment jsdom
import { act, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import useCurrentTime from "./use-current-time";
function CurrentTime({ refreshInterval }) {
return <span>{useCurrentTime(refreshInterval)}</span>;
}
describe("utils/hooks/use-current-time", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(1_000);
});
afterEach(() => {
vi.useRealTimers();
});
it("updates at the requested interval", () => {
render(<CurrentTime refreshInterval={5_000} />);
expect(screen.getByText("1000")).toBeInTheDocument();
act(() => {
vi.advanceTimersByTime(5_000);
});
expect(screen.getByText("6000")).toBeInTheDocument();
});
});
+10 -19
View File
@@ -1,26 +1,17 @@
import { useEffect, useState } from "react";
import { useSyncExternalStore } from "react";
const hasFocus = () => typeof document !== "undefined" && document.hasFocus();
const useWindowFocus = () => {
const [focused, setFocused] = useState(hasFocus);
const subscribe = (onFocusChange) => {
window.addEventListener("focus", onFocusChange);
window.addEventListener("blur", onFocusChange);
useEffect(() => {
setFocused(hasFocus());
const onFocus = () => setFocused(true);
const onBlur = () => setFocused(false);
window.addEventListener("focus", onFocus);
window.addEventListener("blur", onBlur);
return () => {
window.removeEventListener("focus", onFocus);
window.removeEventListener("blur", onBlur);
};
}, []);
return focused;
return () => {
window.removeEventListener("focus", onFocusChange);
window.removeEventListener("blur", onFocusChange);
};
};
const useWindowFocus = () => useSyncExternalStore(subscribe, hasFocus, () => false);
export default useWindowFocus;
+3 -1
View File
@@ -12,15 +12,17 @@ function Fixture() {
describe("utils/hooks/window-focus", () => {
it("tracks focus/blur events", async () => {
vi.spyOn(document, "hasFocus").mockReturnValue(true);
const hasFocus = vi.spyOn(document, "hasFocus").mockReturnValue(true);
render(<Fixture />);
expect(screen.getByTestId("focused")).toHaveTextContent("true");
hasFocus.mockReturnValue(false);
window.dispatchEvent(new Event("blur"));
await waitFor(() => expect(screen.getByTestId("focused")).toHaveTextContent("false"));
hasFocus.mockReturnValue(true);
window.dispatchEvent(new Event("focus"));
await waitFor(() => expect(screen.getByTestId("focused")).toHaveTextContent("true"));
});
+6 -6
View File
@@ -2,13 +2,13 @@ import useSWR from "swr";
import { formatProxyUrl } from "./api-helpers";
export default function useWidgetAPI(widget, ...options) {
const config = {};
if (options && options[1]?.refreshInterval) {
config.refreshInterval = options[1].refreshInterval;
export default function useWidgetAPI(widget, endpoint, queryParams, swrConfig = {}) {
const config = { ...swrConfig };
if (queryParams?.refreshInterval) {
config.refreshInterval = queryParams.refreshInterval;
}
let url = formatProxyUrl(widget, ...options);
if (options[0] === "") {
let url = formatProxyUrl(widget, endpoint, queryParams);
if (endpoint === "") {
url = null;
}
const { data, error, mutate } = useSWR(url, config);
+12
View File
@@ -28,6 +28,18 @@ describe("utils/proxy/use-widget-api", () => {
expect(result.mutate).toBe("m");
});
it("passes additional SWR configuration separately from the proxy query", () => {
useSWR.mockReturnValue({ data: undefined, error: undefined, mutate: vi.fn() });
const widget = { service_group: "g", service_name: "s", index: 0 };
const onSuccess = vi.fn();
useWidgetAPI(widget, "status", { refreshInterval: 123 }, { onSuccess });
const [url, config] = useSWR.mock.calls[0];
expect(url).not.toContain("onSuccess");
expect(config).toEqual({ refreshInterval: 123, onSuccess });
});
it("returns data.error as the top-level error", () => {
const dataError = { message: "nope" };
useSWR.mockReturnValue({ data: { error: dataError }, error: undefined, mutate: vi.fn() });
+12
View File
@@ -0,0 +1,12 @@
export default function withWidgetFields(service, defaultFields, maxFields = 4) {
const configuredFields = service.widget.fields;
const fields = (configuredFields?.length ? configuredFields : defaultFields).slice(0, maxFields);
return {
...service,
widget: {
...service.widget,
fields,
},
};
}
+35
View File
@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import withWidgetFields from "./widget-fields";
describe("utils/widget-fields", () => {
it("applies defaults without modifying the service", () => {
const service = { name: "Example", widget: { type: "example" } };
const normalizedService = withWidgetFields(service, ["one", "two"]);
expect(normalizedService).toEqual({
name: "Example",
widget: { type: "example", fields: ["one", "two"] },
});
expect(service).toEqual({ name: "Example", widget: { type: "example" } });
});
it("treats an explicitly empty fields list as unset", () => {
const service = { widget: { type: "example", fields: [] } };
const normalizedService = withWidgetFields(service, ["one", "two"]);
expect(normalizedService.widget.fields).toEqual(["one", "two"]);
});
it("copies and limits configured fields", () => {
const service = { widget: { type: "example", fields: ["one", "two", "three"] } };
const normalizedService = withWidgetFields(service, ["default"], 2);
expect(normalizedService.widget.fields).toEqual(["one", "two"]);
expect(normalizedService.widget.fields).not.toBe(service.widget.fields);
expect(service.widget.fields).toEqual(["one", "two", "three"]);
});
});