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";
-- 
Mailing list: https://launchpad.net/~sts-sponsors
Post to     : sts-sponsors@lists.launchpad.net
Unsubscribe : https://launchpad.net/~sts-sponsors
More help   : https://help.launchpad.net/ListHelp

Reply via email to