From 1f7066a2d9170f0f042c522af60454688f0867e7 Mon Sep 17 00:00:00 2001 From: minhtrannhat Date: Mon, 4 Sep 2023 16:08:44 -0400 Subject: [PATCH] Feat(frontend): React Query for interacting with the backend --- frontend/package-lock.json | 73 ++++++++++++++++++++++++++++++++++ frontend/package.json | 2 + frontend/src/App.tsx | 34 +++++++++------- frontend/src/query.ts | 80 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 175 insertions(+), 14 deletions(-) create mode 100644 frontend/src/query.ts 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, + }, + ); +}