diff --git a/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.test.tsx b/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.test.tsx
new file mode 100644
index 0000000000..b9c736bf61
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.test.tsx
@@ -0,0 +1,76 @@
+import type { FC } from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { testServerRoute, testServerSetup } from 'utils/testServer';
+import { useChangeRequestSearch } from './useChangeRequestSearch.ts';
+import { useSWRConfig } from 'swr';
+
+const server = testServerSetup();
+
+const TestComponent: FC<{ params: { createdBy?: string; limit?: number } }> = ({
+ params,
+}) => {
+ const { loading, error, changeRequests, total, refetch } =
+ useChangeRequestSearch(params);
+ const { cache } = useSWRConfig();
+ if (loading) {
+ return
Loading...
;
+ }
+
+ if (error) {
+ return Error: {error}
;
+ }
+
+ return (
+
+
+
+ Change Requests:{' '}
+ {changeRequests.map((cr) => cr.title).join(', ')}
+
+
Total: {total}
+
Cache: {[...cache.keys()]}
+
+ );
+};
+
+describe('useChangeRequestSearch', () => {
+ test('should overwrite cache total with 0 if the next result has 0 values', async () => {
+ const createdBy = '789';
+ const url = `/api/admin/search/change-requests?createdBy=${createdBy}`;
+ testServerRoute(server, url, {
+ changeRequests: [
+ {
+ id: 1,
+ title: 'Change Request 1',
+ createdAt: '2024-01-01T00:00:00Z',
+ createdBy: { id: 789, username: 'testuser' },
+ environment: 'production',
+ project: 'test-project',
+ features: ['feature1'],
+ segments: [],
+ state: 'Draft',
+ },
+ ],
+ total: 1,
+ });
+
+ const { rerender } = render();
+
+ await screen.findByText(/Total: 1/);
+
+ testServerRoute(server, url, {
+ changeRequests: [],
+ total: 0,
+ });
+
+ // force fetch
+ const button = await screen.findByRole('button', { name: 'refetch' });
+ button.click();
+
+ rerender();
+ await screen.findByText(/Total: 0/);
+ });
+});
diff --git a/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.ts b/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.ts
new file mode 100644
index 0000000000..fca192d487
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.ts
@@ -0,0 +1,127 @@
+import useSWR, { type SWRConfiguration } from 'swr';
+import { useCallback, useEffect } from 'react';
+import { formatApiPath } from 'utils/formatPath';
+import handleErrorResponses from '../httpErrorResponseHandler.js';
+import type {
+ SearchChangeRequestsParams,
+ ChangeRequestSearchResponseSchema,
+} from 'openapi';
+import { useClearSWRCache } from 'hooks/useClearSWRCache';
+
+type UseChangeRequestSearchOutput = {
+ loading: boolean;
+ initialLoad: boolean;
+ error: string;
+ refetch: () => void;
+} & ChangeRequestSearchResponseSchema;
+
+type CacheValue = {
+ total: number;
+ initialLoad: boolean;
+ [key: string]: number | boolean;
+};
+
+type InternalCache = Record;
+
+const fallbackData: ChangeRequestSearchResponseSchema = {
+ changeRequests: [],
+ total: 0,
+};
+
+const SWR_CACHE_SIZE = 10;
+const PATH = 'api/admin/search/change-requests?';
+
+const createChangeRequestSearch = () => {
+ const internalCache: InternalCache = {};
+
+ const initCache = (id: string) => {
+ internalCache[id] = {
+ total: 0,
+ initialLoad: true,
+ };
+ };
+
+ const set = (id: string, key: string, value: number | boolean) => {
+ if (!internalCache[id]) {
+ initCache(id);
+ }
+ internalCache[id][key] = value;
+ };
+
+ const get = (id: string) => {
+ if (!internalCache[id]) {
+ initCache(id);
+ }
+ return internalCache[id];
+ };
+
+ return (
+ params: SearchChangeRequestsParams,
+ options: SWRConfiguration = {},
+ cachePrefix: string = '',
+ ): UseChangeRequestSearchOutput => {
+ const { KEY, fetcher } = getChangeRequestSearchFetcher(params);
+ const swrKey = `${cachePrefix}${KEY}`;
+ const cacheId = 'global';
+ useClearSWRCache(swrKey, PATH, SWR_CACHE_SIZE);
+
+ useEffect(() => {
+ initCache(cacheId);
+ }, []);
+
+ const { data, error, mutate, isLoading } =
+ useSWR(swrKey, fetcher, options);
+
+ const refetch = useCallback(() => {
+ mutate();
+ }, [mutate]);
+
+ const cacheValues = get(cacheId);
+
+ if (data?.total !== undefined) {
+ set(cacheId, 'total', data.total);
+ }
+
+ if (!isLoading && cacheValues.initialLoad) {
+ set(cacheId, 'initialLoad', false);
+ }
+
+ const returnData = data || fallbackData;
+ return {
+ ...returnData,
+ loading: isLoading,
+ error,
+ refetch,
+ total: cacheValues.total,
+ initialLoad: isLoading && cacheValues.initialLoad,
+ };
+ };
+};
+
+export const DEFAULT_PAGE_LIMIT = 25;
+
+const getChangeRequestSearchFetcher = (params: SearchChangeRequestsParams) => {
+ const urlSearchParams = new URLSearchParams(
+ Array.from(
+ Object.entries(params)
+ .filter(([_, value]) => !!value)
+ .map(([key, value]) => [key, value.toString()]),
+ ),
+ ).toString();
+ const KEY = `${PATH}${urlSearchParams}`;
+ const fetcher = () => {
+ const path = formatApiPath(KEY);
+ return fetch(path, {
+ method: 'GET',
+ })
+ .then(handleErrorResponses('Change request search'))
+ .then((res) => res.json());
+ };
+
+ return {
+ fetcher,
+ KEY,
+ };
+};
+
+export const useChangeRequestSearch = createChangeRequestSearch();
diff --git a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts
index 15b0bc232f..8c45e49612 100644
--- a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts
+++ b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts
@@ -63,7 +63,7 @@ const createFeatureSearch = () => {
useClearSWRCache(swrKey, PATH, SWR_CACHE_SIZE);
useEffect(() => {
- initCache(params.project || '');
+ initCache(cacheId);
}, []);
const { data, error, mutate, isLoading } = useSWR(
diff --git a/frontend/src/openapi/models/searchChangeRequestsParams.ts b/frontend/src/openapi/models/searchChangeRequestsParams.ts
index 35d124d6f8..9dec47a76f 100644
--- a/frontend/src/openapi/models/searchChangeRequestsParams.ts
+++ b/frontend/src/openapi/models/searchChangeRequestsParams.ts
@@ -16,9 +16,9 @@ export type SearchChangeRequestsParams = {
/**
* The number of change requests to skip when returning a page. By default it is set to 0.
*/
- offset?: string;
+ offset?: number;
/**
* The number of change requests to return in a page. By default it is set to 50. The maximum is 1000.
*/
- limit?: string;
+ limit?: number;
};