Skip to content

Commit 5445eb6

Browse files
feat: enforce sensible defaults for react-query (#268)
* feat: enforce sensible defaults for react-query * fix: failing test
1 parent ba35ff4 commit 5445eb6

26 files changed

+183
-142
lines changed

eslint.config.js

+50
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ import reactRefresh from "eslint-plugin-react-refresh";
55
import tseslint from "typescript-eslint";
66
import tailwindPlugin from "eslint-plugin-tailwindcss";
77

8+
const restrictedSyntax = {
9+
reactQuery: {
10+
useQuery: (v) =>
11+
`CallExpression[callee.name='useQuery'] > ObjectExpression:first-child > Property[key.name='${v}']`,
12+
useQueries: (v) =>
13+
`CallExpression[callee.name='useQueries'] > ObjectExpression:first-child > Property[key.name='queries'] > ArrayExpression > ObjectExpression > Property[key.name='${v}']`,
14+
},
15+
};
16+
817
export default tseslint.config(
918
{ ignores: ["dist"] },
1019
{
@@ -56,6 +65,47 @@ export default tseslint.config(
5665
],
5766
},
5867
],
68+
"no-restricted-syntax": [
69+
"error",
70+
{
71+
selector: [
72+
restrictedSyntax.reactQuery.useQuery("staleTime"),
73+
restrictedSyntax.reactQuery.useQuery("gcTime"),
74+
restrictedSyntax.reactQuery.useQueries("staleTime"),
75+
restrictedSyntax.reactQuery.useQueries("gcTime"),
76+
].join(", "),
77+
message:
78+
"`staleTime` & `gcTime` should be managed via the `getQueryCacheConfig` util instead.",
79+
},
80+
{
81+
selector: [
82+
restrictedSyntax.reactQuery.useQuery("queryKey"),
83+
restrictedSyntax.reactQuery.useQuery("queryFn"),
84+
restrictedSyntax.reactQuery.useQueries("queryKey"),
85+
restrictedSyntax.reactQuery.useQueries("queryFn"),
86+
].join(", "),
87+
message:
88+
"'queryKey' & 'queryFn' should be managed by openapi-ts react-query integration instead. This allows standardized management of query keys & cache invalidation.",
89+
},
90+
{
91+
selector: [
92+
restrictedSyntax.reactQuery.useQuery("refetchOnMount"),
93+
restrictedSyntax.reactQuery.useQuery("refetchOnReconnect"),
94+
restrictedSyntax.reactQuery.useQuery("refetchOnWindowFocus"),
95+
restrictedSyntax.reactQuery.useQueries("refetchOnMount"),
96+
restrictedSyntax.reactQuery.useQueries("refetchOnReconnect"),
97+
restrictedSyntax.reactQuery.useQueries("refetchOnWindowFocus"),
98+
].join(", "),
99+
message:
100+
"`refetchOnMount`, `refetchOnReconnect` & `refetchOnWindowFocus` should be managed centrally in the react-query provider",
101+
},
102+
{
103+
selector:
104+
"CallExpression > MemberExpression[property.name='invalidateQueries']",
105+
message:
106+
"Do not directly call `invalidateQueries`. Instead, use the `invalidateQueries` helper function.",
107+
},
108+
],
59109
"no-restricted-imports": [
60110
"error",
61111
{

src/App.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { Header } from "./features/header/components/header";
22
import { PromptList } from "./components/PromptList";
3-
import { usePromptsData } from "./hooks/usePromptsData";
3+
import { useQueryGetWorkspaceMessages } from "./hooks/use-query-get-workspace-messages";
44
import { Sidebar } from "./components/Sidebar";
55
import { useSse } from "./hooks/useSse";
66
import Page from "./Page";
77

88
function App() {
9-
const { data: prompts, isLoading } = usePromptsData();
9+
const { data: prompts, isLoading } = useQueryGetWorkspaceMessages();
1010
useSse();
1111

1212
return (

src/components/react-query-provider.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ export function QueryClientProvider({ children }: { children: ReactNode }) {
4242
new QueryClient({
4343
defaultOptions: {
4444
queries: {
45-
...getQueryCacheConfig("short"),
45+
...getQueryCacheConfig("no-cache"),
46+
refetchOnMount: true,
47+
refetchOnReconnect: true,
48+
refetchOnWindowFocus: true,
4649
},
4750
},
4851
}),
@@ -65,6 +68,7 @@ export function QueryClientProvider({ children }: { children: ReactNode }) {
6568

6669
setActiveWorkspaceName(newWorkspaceName);
6770

71+
// eslint-disable-next-line no-restricted-syntax
6872
void queryClient.invalidateQueries({
6973
refetchType: "all",
7074
// Avoid a continuous loop

src/features/alerts/hooks/use-query-get-workspace-alerts.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
} from "@/api/generated";
55
import { v1GetWorkspaceAlertsOptions } from "@/api/generated/@tanstack/react-query.gen";
66
import { useActiveWorkspaceName } from "@/features/workspace/hooks/use-active-workspace-name";
7+
import { getQueryCacheConfig } from "@/lib/react-query-utils";
78
import { useQuery } from "@tanstack/react-query";
89

910
export function useQueryGetWorkspaceAlerts<T = V1GetWorkspaceAlertsResponse>({
@@ -33,9 +34,7 @@ export function useQueryGetWorkspaceAlerts<T = V1GetWorkspaceAlertsResponse>({
3334
...rest
3435
} = useQuery({
3536
...v1GetWorkspaceAlertsOptions(options),
36-
refetchOnMount: true,
37-
refetchOnReconnect: true,
38-
refetchOnWindowFocus: true,
37+
...getQueryCacheConfig("5s"),
3938
select,
4039
});
4140

src/features/alerts/hooks/use-query-get-workspace-token-usage.ts

-3
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ export function useQueryGetWorkspaceTokenUsage<
2323

2424
return useQuery({
2525
...v1GetWorkspaceTokenUsageOptions(options),
26-
refetchOnMount: true,
27-
refetchOnReconnect: true,
28-
refetchOnWindowFocus: true,
2926
select,
3027
});
3128
}

src/features/header/components/header-status-menu.tsx

+10-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { useCodeGateStatus } from "../hooks/use-codegate-status";
2-
import { HealthStatus } from "../types";
1+
import { useQueriesCodegateStatus } from "../hooks/use-queries-codegate-status";
32
import {
43
Button,
54
DialogTrigger,
@@ -40,27 +39,27 @@ type CodeGateHealthCheckStatus =
4039
| "error_checking_health";
4140

4241
function deriveOverallStatus(
43-
data: ReturnType<typeof useCodeGateStatus>["data"],
42+
data: ReturnType<typeof useQueriesCodegateStatus>["data"],
4443
isPending: boolean,
4544
isError: boolean,
4645
): CodeGateStatus {
4746
if (isPending) return "loading";
4847
if (isError) return "error_checking_status";
4948

5049
if (
51-
data?.health === HealthStatus.HEALTHY &&
50+
data?.health?.status === "healthy" &&
5251
data.version?.error === null &&
5352
data.version?.is_latest === false
5453
)
5554
return "update_available";
5655

57-
if (data?.health === HealthStatus.HEALTHY) return "healthy";
56+
if (data?.health?.status === "healthy") return "healthy";
5857

5958
return "unhealthy";
6059
}
6160

6261
function deriveVersionStatus(
63-
data: ReturnType<typeof useCodeGateStatus>["data"],
62+
data: ReturnType<typeof useQueriesCodegateStatus>["data"],
6463
isPending: boolean,
6564
isError: boolean,
6665
): CodeGateVersionStatus {
@@ -72,14 +71,14 @@ function deriveVersionStatus(
7271
}
7372

7473
function deriveHealthCheckStatus(
75-
data: ReturnType<typeof useCodeGateStatus>["data"],
74+
data: ReturnType<typeof useQueriesCodegateStatus>["data"],
7675
isPending: boolean,
7776
isError: boolean,
7877
): CodeGateHealthCheckStatus {
7978
if (isPending) return "loading";
8079
if (isError) return "error_checking_health";
8180

82-
if (data?.health == HealthStatus.HEALTHY) return "healthy";
81+
if (data?.health?.status === "healthy") return "healthy";
8382
return "unhealthy";
8483
}
8584

@@ -102,7 +101,7 @@ function getButtonText(status: CodeGateStatus): string {
102101

103102
function getVersionText(
104103
status: CodeGateVersionStatus,
105-
data: ReturnType<typeof useCodeGateStatus>["data"],
104+
data: ReturnType<typeof useQueriesCodegateStatus>["data"],
106105
): ReactNode {
107106
switch (status) {
108107
case "error_checking_version":
@@ -262,7 +261,7 @@ function StatusPopover({
262261
}: {
263262
versionStatus: CodeGateVersionStatus;
264263
healthCheckStatus: CodeGateHealthCheckStatus;
265-
data: ReturnType<typeof useCodeGateStatus>["data"];
264+
data: ReturnType<typeof useQueriesCodegateStatus>["data"];
266265
}) {
267266
return (
268267
<Popover className="px-3 py-2 min-w-64" placement="bottom end">
@@ -290,7 +289,7 @@ function StatusPopover({
290289
}
291290

292291
export function HeaderStatusMenu() {
293-
const { data, isPending, isError } = useCodeGateStatus();
292+
const { data, isPending, isError } = useQueriesCodegateStatus();
294293

295294
const status = deriveOverallStatus(data, isPending, isError);
296295
const versionStatus = deriveVersionStatus(data, isPending, isError);

src/features/header/hooks/use-codegate-status.ts

-49
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useQueries } from "@tanstack/react-query";
2+
3+
import {
4+
HealthCheckHealthGetResponse,
5+
V1VersionCheckResponse,
6+
} from "@/api/generated";
7+
import { VersionResponse } from "../types";
8+
import { getQueryCacheConfig } from "@/lib/react-query-utils";
9+
import {
10+
healthCheckHealthGetOptions,
11+
v1VersionCheckOptions,
12+
} from "@/api/generated/@tanstack/react-query.gen";
13+
import { QueryResult } from "@/types/react-query";
14+
15+
type UseQueryReturn = [
16+
QueryResult<HealthCheckHealthGetResponse>,
17+
QueryResult<V1VersionCheckResponse>,
18+
];
19+
20+
const combine = (results: UseQueryReturn) => {
21+
const [health, version] = results;
22+
23+
return {
24+
data: {
25+
health: health.data as { status: "healthy" } | null,
26+
version: version.data as VersionResponse | null,
27+
},
28+
isError: results.some((r) => r.isError),
29+
isPending: results.some((r) => r.isPending),
30+
isFetching: results.some((r) => r.isFetching),
31+
isLoading: results.some((r) => r.isLoading),
32+
isRefetching: results.some((r) => r.isRefetching),
33+
};
34+
};
35+
36+
export const useQueriesCodegateStatus = () => {
37+
return useQueries({
38+
combine,
39+
queries: [
40+
{
41+
...healthCheckHealthGetOptions(),
42+
refetchInterval: 60_000,
43+
refetchIntervalInBackground: true,
44+
retry: false,
45+
...getQueryCacheConfig("indefinite"),
46+
},
47+
{
48+
...v1VersionCheckOptions(),
49+
refetchInterval: 60_000,
50+
refetchIntervalInBackground: true,
51+
retry: false,
52+
...getQueryCacheConfig("indefinite"),
53+
},
54+
],
55+
});
56+
};

src/features/header/types.ts

-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
export enum HealthStatus {
2-
HEALTHY = "Healthy",
3-
UNHEALTHY = "Unhealthy",
4-
}
5-
61
export type VersionResponse = {
72
current_version: string;
83
latest_version: string;

src/features/providers/hooks/use-invalidate-providers-queries.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import { v1ListProviderEndpointsQueryKey } from "@/api/generated/@tanstack/react-query.gen";
22
import { useQueryClient } from "@tanstack/react-query";
33
import { useCallback } from "react";
4+
import { invalidateQueries } from "../../../lib/react-query-utils";
45

56
export function useInvalidateProvidersQueries() {
67
const queryClient = useQueryClient();
78

89
const invalidate = useCallback(async () => {
9-
await queryClient.invalidateQueries({
10-
queryKey: v1ListProviderEndpointsQueryKey(),
11-
refetchType: "all",
12-
});
10+
invalidateQueries(queryClient, [v1ListProviderEndpointsQueryKey]);
1311
}, [queryClient]);
1412

1513
return invalidate;

src/features/providers/hooks/use-providers.ts

-3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,5 @@ import { v1ListProviderEndpointsOptions } from "@/api/generated/@tanstack/react-
44
export function useProviders() {
55
return useQuery({
66
...v1ListProviderEndpointsOptions(),
7-
refetchOnMount: true,
8-
refetchOnReconnect: true,
9-
refetchOnWindowFocus: true,
107
});
118
}

src/features/workspace/components/workspace-custom-instructions.tsx

+5-6
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { useMutationSetWorkspaceCustomInstructions } from "../hooks/use-mutation
5151
import Fuse from "fuse.js";
5252
import systemPrompts from "../constants/built-in-system-prompts.json";
5353
import { MessageTextSquare02, SearchMd } from "@untitled-ui/icons-react";
54+
import { invalidateQueries } from "@/lib/react-query-utils";
5455

5556
type DarkModeContextValue = {
5657
preference: "dark" | "light" | null;
@@ -290,12 +291,10 @@ export function WorkspaceCustomInstructions({
290291
mutateAsync(
291292
{ ...options, body: { prompt: value } },
292293
{
293-
onSuccess: () => {
294-
queryClient.invalidateQueries({
295-
queryKey: v1GetWorkspaceCustomInstructionsQueryKey(options),
296-
refetchType: "all",
297-
});
298-
},
294+
onSuccess: () =>
295+
invalidateQueries(queryClient, [
296+
v1GetWorkspaceCustomInstructionsQueryKey,
297+
]),
299298
},
300299
);
301300
},

src/features/workspace/components/workspaces-selection.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export function WorkspacesSelection() {
3737

3838
const handleWorkspaceClick = (name: string) => {
3939
activateWorkspace({ body: { name } }).then(() => {
40-
queryClient.invalidateQueries({ refetchType: "all" });
40+
// eslint-disable-next-line no-restricted-syntax
41+
queryClient.invalidateQueries({ refetchType: "all" }); // Global setting, refetch **everything**
4142
setIsOpen(false);
4243
});
4344
};

src/features/workspace/hooks/use-active-workspaces.ts

-3
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ export function useActiveWorkspaces<T = ListActiveWorkspacesResponse>({
1111
...v1ListActiveWorkspacesOptions(),
1212
refetchInterval: 5_000,
1313
refetchIntervalInBackground: true,
14-
refetchOnMount: true,
15-
refetchOnReconnect: true,
16-
refetchOnWindowFocus: true,
1714
retry: false,
1815
select,
1916
});

0 commit comments

Comments
 (0)