diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 6cb0bd3..ff1abe5 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -15,6 +15,7 @@
"@mui/lab": "^5.0.0-alpha.91",
"@mui/material": "^5.9.1",
"@mui/x-date-pickers": "^5.0.0-beta.2",
+ "@tanstack/react-query": "^4.33.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
@@ -22,6 +23,7 @@
"@types/node": "^16.11.45",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
+ "axios": "^1.5.0",
"date-fns": "^2.29.1",
"formik": "^2.2.9",
"react": "^18.2.0",
@@ -3953,6 +3955,41 @@
"url": "https://github.com/sponsors/gregberge"
}
},
+ "node_modules/@tanstack/query-core": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.33.0.tgz",
+ "integrity": "sha512-qYu73ptvnzRh6se2nyBIDHGBQvPY1XXl3yR769B7B6mIDD7s+EZhdlWHQ67JI6UOTFRaI7wupnTnwJ3gE0Mr/g==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.33.0.tgz",
+ "integrity": "sha512-97nGbmDK0/m0B86BdiXzx3EW9RcDYKpnyL2+WwyuLHEgpfThYAnXFaMMmnTDuAO4bQJXEhflumIEUfKmP7ESGA==",
+ "dependencies": {
+ "@tanstack/query-core": "4.33.0",
+ "use-sync-external-store": "^1.2.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-native": "*"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@testing-library/dom": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz",
@@ -5379,6 +5416,29 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
+ "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==",
+ "dependencies": {
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@@ -13438,6 +13498,11 @@
"node": ">= 0.10"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@@ -15914,6 +15979,14 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 721538f..78998e4 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,6 +10,7 @@
"@mui/lab": "^5.0.0-alpha.91",
"@mui/material": "^5.9.1",
"@mui/x-date-pickers": "^5.0.0-beta.2",
+ "@tanstack/react-query": "^4.33.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
@@ -17,6 +18,7 @@
"@types/node": "^16.11.45",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
+ "axios": "^1.5.0",
"date-fns": "^2.29.1",
"formik": "^2.2.9",
"react": "^18.2.0",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index b4555a2..11abe31 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,6 +1,8 @@
import React from "react";
import "./App.css";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+
// Roboto font and its weights
import "@fontsource/roboto/300.css";
import "@fontsource/roboto/400.css";
@@ -22,21 +24,25 @@ import { AuthContextProvider } from "./AuthContext";
// React router
import Router from "./Router";
-function App() {
+const queryClient = new QueryClient();
+
+const App = () => {
return (
-
-
-
- Todo
-
-
-
-
-
-
-
-
+
+
+
+
+ Todo
+
+
+
+
+
+
+
+
+
);
-}
+};
export default App;
diff --git a/frontend/src/query.ts b/frontend/src/query.ts
new file mode 100644
index 0000000..5cc3c6a
--- /dev/null
+++ b/frontend/src/query.ts
@@ -0,0 +1,80 @@
+import axios, { AxiosError } from "axios";
+import { useContext } from "react";
+import {
+ MutationFunction,
+ QueryFunction,
+ QueryFunctionContext,
+ QueryKey,
+ useMutation as useReactMutation,
+ UseMutationOptions,
+ UseMutationResult,
+ useQuery as useReactQuery,
+ UseQueryOptions,
+ UseQueryResult,
+} from "@tanstack/react-query";
+
+import { AuthContext } from "src/AuthContext";
+
+const MAX_FAILURES = 2;
+
+export function useQuery<
+ TQueryFnData = unknown,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+>(
+ queryKey: TQueryKey,
+ queryFn: QueryFunction,
+ options?: UseQueryOptions,
+): UseQueryResult {
+ const { setAuthenticated } = useContext(AuthContext);
+
+ return useReactQuery(
+ queryKey,
+ async (context: QueryFunctionContext) => {
+ try {
+ return await queryFn(context);
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response?.status === 401) {
+ setAuthenticated(false);
+ }
+ throw error;
+ }
+ },
+ {
+ retry: (failureCount: number, error: AxiosError) =>
+ failureCount < MAX_FAILURES &&
+ (!error.response || error.response.status >= 500),
+ ...options,
+ },
+ );
+}
+
+export function useMutation<
+ TData = unknown,
+ TVariables = void,
+ TContext = unknown,
+>(
+ mutationFn: MutationFunction,
+ options?: UseMutationOptions,
+): UseMutationResult {
+ const { setAuthenticated } = useContext(AuthContext);
+
+ return useReactMutation(
+ async (variables: TVariables) => {
+ try {
+ return await mutationFn(variables);
+ } catch (error) {
+ if (axios.isAxiosError(error) && error.response?.status === 401) {
+ setAuthenticated(false);
+ }
+ throw error;
+ }
+ },
+ {
+ retry: (failureCount: number, error: AxiosError) =>
+ failureCount < MAX_FAILURES &&
+ (!error.response || error.response.status >= 500),
+ ...options,
+ },
+ );
+}