sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #05930
[Merge] ~petermakowski/maas-site-manager:debounce-search-requests-MAASENG-1481 into maas-site-manager:main
Peter Makowski has proposed merging ~petermakowski/maas-site-manager:debounce-search-requests-MAASENG-1481 into maas-site-manager:main.
Commit message:
feat: debounce search requests
- add useDebouncedValue hook
- add @testing-library/react-hooks
Requested reviews:
MAAS Committers (maas-committers)
For more details, see:
https://code.launchpad.net/~petermakowski/maas-site-manager/+git/site-manager/+merge/438880
--
Your team MAAS Committers is requested to review the proposed merge of ~petermakowski/maas-site-manager:debounce-search-requests-MAASENG-1481 into maas-site-manager:main.
diff --git a/frontend/package.json b/frontend/package.json
index 75a5826..757e451 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -32,6 +32,7 @@
"@remix-run/web-fetch": "4.3.2",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
+ "@testing-library/react-hooks": "8.0.1",
"@testing-library/user-event": "14.4.3",
"@types/axios": "0.14.0",
"@types/chance": "1.1.3",
diff --git a/frontend/src/components/Navigation/Navigation.test.tsx b/frontend/src/components/Navigation/Navigation.test.tsx
index 549dcdc..ab2baed 100644
--- a/frontend/src/components/Navigation/Navigation.test.tsx
+++ b/frontend/src/components/Navigation/Navigation.test.tsx
@@ -1,9 +1,8 @@
-import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router-dom";
import Navigation from "./Navigation";
-import { render, screen } from "@/test-utils";
+import { render, screen, userEvent } from "@/test-utils";
describe("Navigation", () => {
it("displays navigation", () => {
diff --git a/frontend/src/components/SitesList/SitesList.tsx b/frontend/src/components/SitesList/SitesList.tsx
index 8529e5f..f47b9ed 100644
--- a/frontend/src/components/SitesList/SitesList.tsx
+++ b/frontend/src/components/SitesList/SitesList.tsx
@@ -5,6 +5,7 @@ import { Pagination } from "@canonical/react-components";
import SitesTable from "./components/SitesTable";
import { useSitesQuery } from "@/hooks/api";
+import useDebounce from "@/hooks/useDebouncedValue";
import { parseSearchTextToQueryParams } from "@/utils";
const DEFAULT_PAGE_SIZE = 50;
@@ -13,9 +14,11 @@ const SitesList = () => {
const [page, setPage] = useState(0);
const [size] = useState(DEFAULT_PAGE_SIZE);
const [searchText, setSearchText] = useState("");
+ const debounceSearchText = useDebounce(searchText);
+
const { data, isLoading, isFetchedAfterMount } = useSitesQuery(
{ page: `${page}`, size: `${size}` },
- parseSearchTextToQueryParams(searchText),
+ parseSearchTextToQueryParams(debounceSearchText),
);
useEffect(() => {
diff --git a/frontend/src/hooks/useDebouncedValue.test.ts b/frontend/src/hooks/useDebouncedValue.test.ts
new file mode 100644
index 0000000..ef6d4b8
--- /dev/null
+++ b/frontend/src/hooks/useDebouncedValue.test.ts
@@ -0,0 +1,50 @@
+import { vi } from "vitest";
+
+import useDebouncedValue from "./useDebouncedValue";
+
+import { renderHook } from "@/test-utils";
+
+describe("useDebouncedValue", () => {
+ beforeAll(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.clearAllTimers();
+ });
+
+ afterAll(() => {
+ vi.useRealTimers();
+ });
+
+ it("returns debounced value", async () => {
+ const { result, rerender } = renderHook(({ value }) => useDebouncedValue(value), {
+ initialProps: {
+ value: "value",
+ },
+ });
+
+ expect(result.current).toBe("value");
+
+ await rerender({ value: "new-value" });
+ await vi.advanceTimersToNextTimer();
+
+ expect(result.current).toBe("new-value");
+ });
+
+ it("accepts custom delay", async () => {
+ const { result, rerender } = renderHook(({ value, delay }) => useDebouncedValue(value, delay), {
+ initialProps: {
+ value: "value",
+ delay: 5,
+ },
+ });
+
+ expect(result.current).toBe("value");
+
+ await rerender({ value: "new-value", delay: 5 });
+ await vi.advanceTimersByTime(5);
+
+ expect(result.current).toBe("new-value");
+ });
+});
diff --git a/frontend/src/hooks/useDebouncedValue.ts b/frontend/src/hooks/useDebouncedValue.ts
new file mode 100644
index 0000000..509f673
--- /dev/null
+++ b/frontend/src/hooks/useDebouncedValue.ts
@@ -0,0 +1,18 @@
+import { useEffect, useState } from "react";
+
+export const DEFAULT_DELAY = 500;
+
+function useDebouncedValue<T>(value: T, delay?: number): T {
+ const [debouncedValue, setDebouncedValue] = useState<T>(value);
+
+ useEffect(() => {
+ const timeoutId = setTimeout(() => setDebouncedValue(value), delay || DEFAULT_DELAY);
+ return () => {
+ clearTimeout(timeoutId);
+ };
+ }, [value, delay]);
+
+ return debouncedValue;
+}
+
+export default useDebouncedValue;
diff --git a/frontend/src/test-utils.tsx b/frontend/src/test-utils.tsx
index 50437b8..450b8d7 100644
--- a/frontend/src/test-utils.tsx
+++ b/frontend/src/test-utils.tsx
@@ -44,5 +44,7 @@ const renderWithMemoryRouter = (ui: ReactElement, options?: MemoryRenderOptions)
export { screen, within, waitFor } from "@testing-library/react";
export { customRender as render };
+export { renderHook } from "@testing-library/react-hooks";
+export { default as userEvent } from "@testing-library/user-event";
export { renderWithMemoryRouter };
export { Providers };
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 5c9a9a9..5f07629 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1549,6 +1549,14 @@
lodash "^4.17.15"
redent "^3.0.0"
+"@testing-library/react-hooks@8.0.1":
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12"
+ integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==
+ dependencies:
+ "@babel/runtime" "^7.12.5"
+ react-error-boundary "^3.1.0"
+
"@testing-library/react@13.4.0":
version "13.4.0"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.4.0.tgz#6a31e3bf5951615593ad984e96b9e5e2d9380966"
@@ -5167,6 +5175,13 @@ react-dom@18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
+react-error-boundary@^3.1.0:
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0"
+ integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==
+ dependencies:
+ "@babel/runtime" "^7.12.5"
+
react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
Follow ups