-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(base): setup react query MAASENG-3401 (#5484)
refactor(base): setup react query MAASENG-3401 - react Query integration - add websocket-aware query - add zones-related hooks - integrate React Query DevTools
- Loading branch information
1 parent
7e3cb5d
commit 3040e2b
Showing
14 changed files
with
371 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
VITE_APP_CHECK_MIDDLEWARE=true | ||
VITE_APP_WEBSOCKET_DEBUG=true | ||
VITE_APP_REACT_QUERY_DEVTOOLS=false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { it, expect, vi, type Mock } from "vitest"; | ||
|
||
import { DEFAULT_HEADERS, fetchWithAuth } from "./base"; | ||
|
||
import { getCookie } from "@/app/utils"; | ||
|
||
vi.mock("@/app/utils", () => ({ getCookie: vi.fn() })); | ||
|
||
const mockCsrfToken = "mock-csrf-token"; | ||
const url = "https://example.com/api"; | ||
|
||
const originalFetch = global.fetch; | ||
|
||
beforeEach(() => { | ||
(getCookie as Mock).mockReturnValue(mockCsrfToken); | ||
}); | ||
|
||
afterAll(() => { | ||
global.fetch = originalFetch; | ||
}); | ||
|
||
it("should call fetch with correct parameters", async () => { | ||
const mockResponse = { | ||
ok: true, | ||
json: vi.fn().mockResolvedValue({ data: "test" }), | ||
}; | ||
global.fetch = vi.fn().mockResolvedValue(mockResponse); | ||
|
||
const options = { method: "POST", body: JSON.stringify({ test: true }) }; | ||
const result = await fetchWithAuth(url, options); | ||
|
||
expect(fetch).toHaveBeenCalledWith(url, { | ||
...options, | ||
headers: { ...DEFAULT_HEADERS, "X-CSRFToken": mockCsrfToken }, | ||
}); | ||
expect(result).toEqual({ data: "test" }); | ||
}); | ||
|
||
it("should handle errors", async () => { | ||
global.fetch = vi | ||
.fn() | ||
.mockResolvedValue({ ok: false, statusText: "Bad Request" }); | ||
await expect(fetchWithAuth(url)).rejects.toThrow("Bad Request"); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { getCookie } from "@/app/utils"; | ||
|
||
export const ROOT_API = "/MAAS/api/2.0/"; | ||
|
||
export const DEFAULT_HEADERS = { | ||
"Content-Type": "application/json", | ||
Accept: "application/json", | ||
}; | ||
|
||
export const handleErrors = (response: Response) => { | ||
if (!response.ok) { | ||
throw Error(response.statusText); | ||
} | ||
return response; | ||
}; | ||
|
||
export const fetchWithAuth = async (url: string, options: RequestInit = {}) => { | ||
const csrftoken = getCookie("csrftoken"); | ||
const headers = { | ||
...DEFAULT_HEADERS, | ||
"X-CSRFToken": csrftoken || "", | ||
...options.headers, | ||
}; | ||
|
||
const response = await fetch(url, { ...options, headers }); | ||
return handleErrors(response).json(); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { ROOT_API, fetchWithAuth } from "@/app/api/base"; | ||
import type { Zone } from "@/app/store/zone/types"; | ||
|
||
export const fetchZones = (): Promise<Zone[]> => | ||
fetchWithAuth(`${ROOT_API}zones/`); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { QueryClient } from "@tanstack/react-query"; | ||
|
||
export const queryKeys = { | ||
zones: { | ||
list: ["zones"], | ||
}, | ||
} as const; | ||
|
||
type QueryKeys = typeof queryKeys; | ||
type QueryKeyCategories = keyof QueryKeys; | ||
type QueryKeySubcategories<T extends QueryKeyCategories> = keyof QueryKeys[T]; | ||
|
||
export type QueryKey = | ||
QueryKeys[QueryKeyCategories][QueryKeySubcategories<QueryKeyCategories>]; | ||
|
||
export const defaultQueryOptions = { | ||
staleTime: 5 * 60 * 1000, // 5 minutes | ||
cacheTime: 15 * 60 * 1000, // 15 minutes | ||
refetchOnWindowFocus: true, | ||
} as const; | ||
|
||
export const realTimeQueryOptions = { | ||
staleTime: 0, | ||
cacheTime: 60 * 1000, // 1 minute | ||
} as const; | ||
|
||
export const createQueryClient = () => | ||
new QueryClient({ | ||
defaultOptions: { | ||
queries: defaultQueryOptions, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import * as reactQuery from "@tanstack/react-query"; | ||
|
||
import { useWebsocketAwareQuery } from "./base"; | ||
|
||
import { rootState, statusState } from "@/testing/factories"; | ||
import { renderHookWithMockStore } from "@/testing/utils"; | ||
|
||
vi.mock("@tanstack/react-query"); | ||
|
||
const mockQueryFn = vi.fn(); | ||
const mockQueryKey = ["zones"] as const; | ||
|
||
beforeEach(() => { | ||
vi.resetAllMocks(); | ||
const mockQueryClient: Partial<reactQuery.QueryClient> = { | ||
invalidateQueries: vi.fn(), | ||
}; | ||
vi.mocked(reactQuery.useQueryClient).mockReturnValue( | ||
mockQueryClient as reactQuery.QueryClient | ||
); | ||
vi.mocked(reactQuery.useQuery).mockReturnValue({ | ||
data: "testData", | ||
isLoading: false, | ||
} as reactQuery.UseQueryResult); | ||
}); | ||
|
||
it("calls useQuery with correct parameters", () => { | ||
renderHookWithMockStore(() => | ||
useWebsocketAwareQuery(mockQueryKey, mockQueryFn) | ||
); | ||
expect(reactQuery.useQuery).toHaveBeenCalledWith({ | ||
queryKey: mockQueryKey, | ||
queryFn: mockQueryFn, | ||
}); | ||
}); | ||
|
||
it("invalidates queries when connectedCount changes", () => { | ||
const initialState = rootState({ | ||
status: statusState({ connectedCount: 0 }), | ||
}); | ||
const { rerender } = renderHookWithMockStore( | ||
() => useWebsocketAwareQuery(mockQueryKey, mockQueryFn), | ||
{ initialState } | ||
); | ||
|
||
const mockInvalidateQueries = vi.fn(); | ||
const mockQueryClient: Partial<reactQuery.QueryClient> = { | ||
invalidateQueries: mockInvalidateQueries, | ||
}; | ||
vi.mocked(reactQuery.useQueryClient).mockReturnValue( | ||
mockQueryClient as reactQuery.QueryClient | ||
); | ||
|
||
rerender({ | ||
initialState: rootState({ status: statusState({ connectedCount: 1 }) }), | ||
}); | ||
expect(mockInvalidateQueries).toHaveBeenCalled(); | ||
}); | ||
|
||
it("returns the result of useQuery", () => { | ||
const { result } = renderHookWithMockStore(() => | ||
useWebsocketAwareQuery(mockQueryKey, mockQueryFn) | ||
); | ||
expect(result.current).toEqual({ data: "testData", isLoading: false }); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { useEffect } from "react"; | ||
|
||
import type { QueryFunction, UseQueryOptions } from "@tanstack/react-query"; | ||
import { useQuery, useQueryClient } from "@tanstack/react-query"; | ||
import { useSelector } from "react-redux"; | ||
|
||
import type { QueryKey } from "@/app/api/query-client"; | ||
import statusSelectors from "@/app/store/status/selectors"; | ||
|
||
export function useWebsocketAwareQuery< | ||
TQueryFnData = unknown, | ||
TError = unknown, | ||
TData = TQueryFnData, | ||
>( | ||
queryKey: QueryKey, | ||
queryFn: QueryFunction<TQueryFnData>, | ||
options?: Omit< | ||
UseQueryOptions<TQueryFnData, TError, TData>, | ||
"queryKey" | "queryFn" | ||
> | ||
) { | ||
const queryClient = useQueryClient(); | ||
const connectedCount = useSelector(statusSelectors.connectedCount); | ||
|
||
useEffect(() => { | ||
queryClient.invalidateQueries(); | ||
}, [connectedCount, queryClient, queryKey]); | ||
|
||
return useQuery<TQueryFnData, TError, TData>({ | ||
queryKey, | ||
queryFn, | ||
...options, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import type { UseQueryResult } from "@tanstack/react-query"; | ||
|
||
import { useItemsCount } from "./utils"; | ||
|
||
import { renderHook } from "@/testing/utils"; | ||
|
||
it("should return 0 when data is undefined", () => { | ||
const mockUseItems = vi.fn( | ||
() => ({ data: undefined }) as UseQueryResult<any[], unknown> | ||
); | ||
const { result } = renderHook(() => useItemsCount(mockUseItems)); | ||
expect(result.current).toBe(0); | ||
}); | ||
|
||
it("should return the correct count when data is available", () => { | ||
const mockData = [1, 2, 3, 4, 5]; | ||
const mockUseItems = vi.fn( | ||
() => ({ data: mockData }) as UseQueryResult<number[], unknown> | ||
); | ||
const { result } = renderHook(() => useItemsCount(mockUseItems)); | ||
expect(result.current).toBe(5); | ||
}); | ||
|
||
it("should return 0 when data is an empty array", () => { | ||
const mockUseItems = vi.fn(); | ||
mockUseItems.mockReturnValueOnce({ data: [] } as UseQueryResult<[], unknown>); | ||
const { result } = renderHook(() => useItemsCount(mockUseItems)); | ||
expect(result.current).toBe(0); | ||
}); | ||
|
||
it("should update count when data changes", () => { | ||
const mockUseItems = vi.fn(); | ||
mockUseItems.mockReturnValueOnce({ data: [1, 2, 3] } as UseQueryResult< | ||
number[], | ||
unknown | ||
>); | ||
const { result, rerender } = renderHook(() => useItemsCount(mockUseItems)); | ||
expect(result.current).toBe(3); | ||
|
||
mockUseItems.mockReturnValueOnce({ data: [1, 2, 3, 4] } as UseQueryResult< | ||
number[], | ||
unknown | ||
>); | ||
rerender(); | ||
expect(result.current).toBe(4); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { useMemo } from "react"; | ||
|
||
import type { UseQueryResult } from "@tanstack/react-query"; | ||
|
||
type QueryHook<T> = () => UseQueryResult<T[], unknown>; | ||
|
||
export const useItemsCount = <T>(useItems: QueryHook<T>) => { | ||
const { data } = useItems(); | ||
return useMemo(() => data?.length ?? 0, [data]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { fetchZones } from "@/app/api/endpoints"; | ||
import { useWebsocketAwareQuery } from "@/app/api/query/base"; | ||
import { useItemsCount } from "@/app/api/query/utils"; | ||
|
||
export const useZones = () => { | ||
return useWebsocketAwareQuery(["zones"], fetchZones); | ||
}; | ||
|
||
export const useZonesCount = () => useItemsCount(useZones); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.