diff --git a/package-lock.json b/package-lock.json index fae5ed4..570f99a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "@next/eslint-plugin-next": "^14.2.3", "@pgtyped/cli": "^2.3.0", "@stylistic/eslint-plugin": "^2.1.0", + "@tanstack/react-query-devtools": "^5.51.1", "@types/eslint": "^8.56.10", "@types/node": "^20", "@types/react": "^18", @@ -2223,9 +2224,20 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.49.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.49.1.tgz", - "integrity": "sha512-JnC9ndmD1KKS01Rt/ovRUB1tmwO7zkyXAyIxN9mznuJrcNtOrkmOnQqdJF2ib9oHzc2VxHomnEG7xyfo54Npkw==", + "version": "5.51.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.1.tgz", + "integrity": "sha512-fJBMQMpo8/KSsWW5ratJR5+IFr7YNJ3K2kfP9l5XObYHsgfVy1w3FJUWU4FT2fj7+JMaEg33zOcNDBo0LMwHnw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.51.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.51.1.tgz", + "integrity": "sha512-rehG0WmL3EXER6MAI2uHQia/n0b5c3ZROohpYm7u3G7yg4q+HsfQy6nuAo6uy40NzHUe3FmnfWCZQ0Vb/3lE6g==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -2233,12 +2245,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.49.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.49.2.tgz", - "integrity": "sha512-6rfwXDK9BvmHISbNFuGd+wY3P44lyW7lWiA9vIFGT/T0P9aHD1VkjTvcM4SDAIbAQ9ygEZZoLt7dlU1o3NjMVA==", + "version": "5.51.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.1.tgz", + "integrity": "sha512-s47HKFnQ4HOJAHoIiXcpna/roMMPZJPy6fJ6p4ZNVn8+/onlLBEDd1+xc8OnDuwgvecqkZD7Z2mnSRbcWefrKw==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.49.1" + "@tanstack/query-core": "5.51.1" }, "funding": { "type": "github", @@ -2248,6 +2260,24 @@ "react": "^18.0.0" } }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.51.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.51.1.tgz", + "integrity": "sha512-bRShIVKGpUOHpwziGKT8Aq1Ty0lIlGmNI7E0KbGYtmyOaImErpdElTdxfES1bRaI7i/j+mf2hLy+E6q7SrCwPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.51.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.51.1", + "react": "^18 || ^19" + } + }, "node_modules/@tanstack/react-table": { "version": "8.17.3", "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.17.3.tgz", diff --git a/package.json b/package.json index d6b4994..cb25969 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@next/eslint-plugin-next": "^14.2.3", "@pgtyped/cli": "^2.3.0", "@stylistic/eslint-plugin": "^2.1.0", + "@tanstack/react-query-devtools": "^5.51.1", "@types/eslint": "^8.56.10", "@types/node": "^20", "@types/react": "^18", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5dfd1d3..7a3b257 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,7 @@ import { cn } from "@/lib/utils"; import "@/lib/patch-toJSON-BigInt"; import Navbar from "@/components/Navbar"; import Providers from "@/components/Providers"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { CodeIcon } from "lucide-react"; import Link from "next/link"; @@ -41,6 +42,7 @@ export default function RootLayout({ + diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 9f49245..6ea825a 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,73 @@ +import { toast } from "@/components/ui/use-toast"; +import { QueryCache, QueryClient, defaultShouldDehydrateQuery, isServer } from "@tanstack/react-query"; import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; - + export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +let browserQueryClient: QueryClient | undefined = undefined; + +const makeQueryClient = () => { + // const { toast } = useToast(); + return new QueryClient({ + defaultOptions: { + queries: { + // Direct suggestion by tanstack, to prevent over-eager refetching from the client. + staleTime: 60 * 1000, + }, + dehydrate: { + // only successful and pending Queries are included per defaults + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === "pending", + }, + }, + queryCache: new QueryCache({ + onError: (error, query) => { + // TODO: Overall model is fine but need to change how meta is used. + // Idea: Add a `errorTitle` field instead that can be used in place of "Error" below. This gives a top level, succinct explanation. + // `description` becomes whatever value we store inside our error value. This is what needs to be refactored to make all queries play nicely. + // This allows each error to clearly signal its nature while also giving detail where appropriate. The issue of that detail is delegated to the useQuery callsite + // and any component/route that throws errors. + // There may be a more elegant way of expressing this but the general typing of onError's `error` and `query` arguments requires some amount of refinement for safety. + // https://tanstack.com/query/latest/docs/react/reference/QueryCache + let errorMessage = ""; + const meta = query?.meta ?? undefined ; + if (meta) { + // Precondition for this type cast: meta is of type Record where any query with a meta object containing the property `errorMessage` has a value of type string. + errorMessage = meta?.errorMessage as string ?? ""; + } + if (errorMessage !== "") { + toast({ + variant: "destructive", + title: "Error", + description: `${errorMessage}`, + }); + } else { + // TODO: Realistically, this will not be a useful error and should be improved further. + toast({ + variant: "destructive", + title: "Error", + description: `${error.message}`, + }); + } + }, + }), + }); +}; + +export const getQueryClient = () => { + if (isServer) { + // Server: always make a new query client + return makeQueryClient(); + } else { + // Browser: make a new query client if we don't already have one + // This is very important, so we don't re-make a new client if React + // suspends during the initial render. This may not be needed if we + // have a suspense boundary BELOW the creation of the query client + if (!browserQueryClient) browserQueryClient = makeQueryClient(); + return browserQueryClient; + } +};