Fix useMapData debounce loading state
This commit is contained in:
@@ -205,7 +205,7 @@ Issues are grouped by category and ordered roughly by severity. Each entry descr
|
||||
|
||||
---
|
||||
|
||||
### TASK-011 — No `React.memo` on any heavy component
|
||||
### TASK-011 — No `React.memo` on any heavy component (done)
|
||||
|
||||
**Where found:** Every component in `frontend/src/components/` — zero uses of `React.memo` exist in the codebase.
|
||||
|
||||
@@ -222,7 +222,9 @@ Issues are grouped by category and ordered roughly by severity. Each entry descr
|
||||
|
||||
---
|
||||
|
||||
### TASK-012 — `useMapData` sets `loading=true` before the debounce fires
|
||||
### TASK-012 — `useMapData` sets `loading=true` before the debounce fires (done)
|
||||
|
||||
**Where fixed:** `frontend/src/hooks/useMapData.ts`, `frontend/src/hooks/__tests__/useMapData.test.ts`
|
||||
|
||||
**Where found:** `frontend/src/hooks/useMapData.ts`, `load` callback — `setLoading(true)` is called at the top of `load`, but the actual fetch is deferred inside a `setTimeout` of 300 ms.
|
||||
|
||||
|
||||
78
frontend/src/hooks/__tests__/useMapData.test.ts
Normal file
78
frontend/src/hooks/__tests__/useMapData.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { renderHook, act, waitFor } from "@testing-library/react";
|
||||
import type { BansByCountryResponse } from "../../types/map";
|
||||
import { useMapData } from "../useMapData";
|
||||
import * as api from "../../api/map";
|
||||
|
||||
vi.mock("../../api/map");
|
||||
|
||||
describe("useMapData", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("delays loading until the debounced fetch begins", async () => {
|
||||
const fetchMock = vi.mocked(api.fetchBansByCountry);
|
||||
const firstResponse: BansByCountryResponse = {
|
||||
countries: { US: 1 },
|
||||
country_names: { US: "United States" },
|
||||
bans: [],
|
||||
total: 1,
|
||||
};
|
||||
const secondResponse: BansByCountryResponse = {
|
||||
countries: { US: 2 },
|
||||
country_names: { US: "United States" },
|
||||
bans: [],
|
||||
total: 2,
|
||||
};
|
||||
|
||||
let secondResolve: (value: BansByCountryResponse) => void;
|
||||
fetchMock.mockResolvedValueOnce(firstResponse);
|
||||
fetchMock.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<BansByCountryResponse>((resolve) => {
|
||||
secondResolve = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useMapData("24h", "all", "fail2ban"));
|
||||
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 310));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
act(() => {
|
||||
result.current.refresh();
|
||||
});
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
});
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
});
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
|
||||
await act(async () => {
|
||||
secondResolve(secondResponse);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.total).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -57,11 +57,11 @@ export function useMapData(
|
||||
if (debounceRef.current != null) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
// Show loading immediately so the skeleton / spinner appears.
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
debounceRef.current = setTimeout((): void => {
|
||||
// Show loading only when the fetch is about to start.
|
||||
setLoading(true);
|
||||
// Abort any in-flight request from a previous filter selection.
|
||||
abortRef.current?.abort();
|
||||
abortRef.current = new AbortController();
|
||||
|
||||
Reference in New Issue
Block a user