From e00855bf169790e40d78759b0a8f2a877681f884 Mon Sep 17 00:00:00 2001 From: Sebastian Barrenechea Date: Fri, 27 Dec 2024 11:51:17 -0300 Subject: [PATCH] feat(game-details): tanstack query instead of custom hook for data retrieval --- package-lock.json | 57 ++++++++++++++++++++++++ package.json | 2 + src/hooks/use-game-data.ts | 91 ++++++-------------------------------- src/lib/query.ts | 73 ++++++++++++++++++++++++++++++ src/main.tsx | 14 +++--- src/routes/__root.tsx | 12 ++++- 6 files changed, 165 insertions(+), 84 deletions(-) create mode 100644 src/lib/query.ts diff --git a/package-lock.json b/package-lock.json index 476b3a8..8c2a9cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", + "@tanstack/react-query": "^5.62.11", "@tanstack/react-router": "^1.92.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -34,6 +35,7 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@tanstack/react-query-devtools": "^5.62.11", "@tanstack/router-devtools": "^1.92.6", "@tanstack/router-plugin": "^1.91.1", "@types/node": "^22.10.2", @@ -2694,6 +2696,61 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/query-core": { + "version": "5.62.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.9.tgz", + "integrity": "sha512-lwePd8hNYhyQ4nM/iRQ+Wz2cDtspGeZZHFZmCzHJ7mfKXt+9S301fULiY2IR2byJYY6Z03T427E5PoVfMexHjw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.62.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.62.9.tgz", + "integrity": "sha512-b1NZzDLVf6laJsB1Cfm3ieuYzM+WqoO8qpm9v+3Etwd+Ph4zkhUMiT+wcWj5AhEPsXiRodKYiiW048VDNdBxNg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.62.11", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.11.tgz", + "integrity": "sha512-Xb1nw0cYMdtFmwkvH9+y5yYFhXvLRCnXoqlzSw7UkqtCVFq3cG8q+rHZ2Yz1XrC+/ysUaTqbLKJqk95mCgC1oQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.62.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.62.11", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.62.11.tgz", + "integrity": "sha512-i0vKgdM4ORRzqduz7UeUF52UhLrvRp4sNY/DnLsd5NqNyiKct3a0bLQMWE2fqjF5tEExQ0d0xY60ILXW/T62xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.62.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.62.11", + "react": "^18 || ^19" + } + }, "node_modules/@tanstack/react-router": { "version": "1.92.6", "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.92.6.tgz", diff --git a/package.json b/package.json index 5cc83f4..91a3973 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", + "@tanstack/react-query": "^5.62.11", "@tanstack/react-router": "^1.92.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -37,6 +38,7 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@tanstack/react-query-devtools": "^5.62.11", "@tanstack/router-devtools": "^1.92.6", "@tanstack/router-plugin": "^1.91.1", "@types/node": "^22.10.2", diff --git a/src/hooks/use-game-data.ts b/src/hooks/use-game-data.ts index a328ffd..2839236 100644 --- a/src/hooks/use-game-data.ts +++ b/src/hooks/use-game-data.ts @@ -1,82 +1,17 @@ -import { useCallback, useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; -interface GameData { - commonTitle: string; - cover: string | null; - description: string; - developer: string; - discs: number; - genre: string; - id: string | string[]; - languages: string[]; - officialTitle: string; - publisher: string; - region: string; - releaseDate: string; - title: string; -} +import { fetchGameData, gameDataKeys } from "@/lib/query"; export function useGameData(platform: string, region: string, gameId: string) { - const [gameData, setGameData] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const fetchGameData = useCallback(async () => { - if (!platform || !region || !gameId) { - setGameData(null); - setError(null); - return; - } - - setIsLoading(true); - setError(null); - - try { - const apiRegion = mapRegionToApi(region); - const response = await fetch( - `https://psxdata.barrenechea.cl/${apiRegion}/${gameId}.json` - ); - - if (!response.ok) { - throw new Error("Failed to fetch game data"); - } - - const data = (await response.json()) as GameData; - setGameData({ - ...data, - cover: data.cover - ? `https://psxdata.barrenechea.cl/${apiRegion}/covers/${gameId}.${data.cover.split(".").pop()}` - : null, - }); - } catch (err) { - setError((err as Error).message); - setGameData(null); - } finally { - setIsLoading(false); - } - }, [platform, region, gameId]); - - useEffect(() => { - void fetchGameData(); - }, [fetchGameData]); - - return { gameData, isLoading, error }; -} - -function mapRegionToApi(region: string): string { - switch (region.toLowerCase()) { - case "america": - case "usa": - case "ntsc-u": - return "America"; - case "europe": - case "pal": - case "ntsc-pal": - return "Europe"; - case "japan": - case "ntsc-j": - return "Japan"; - default: - return "America"; // Default to America if unknown - } + const query = useQuery({ + queryKey: gameDataKeys.details(region, gameId), + queryFn: () => fetchGameData(region, gameId), + enabled: Boolean(platform && region && gameId), + }); + + return { + gameData: query.data ?? null, + isLoading: query.isLoading, + error: query.error ? query.error.message : null, + }; } diff --git a/src/lib/query.ts b/src/lib/query.ts new file mode 100644 index 0000000..53301d0 --- /dev/null +++ b/src/lib/query.ts @@ -0,0 +1,73 @@ +import { QueryClient } from "@tanstack/react-query"; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, // Data is fresh for 5 minutes + gcTime: 1000 * 60 * 60, // Unused data is garbage collected after 1 hour + retry: 1, + }, + }, +}); + +export const gameDataKeys = { + all: ["game-data"] as const, + details: (region: string, gameId: string) => + [...gameDataKeys.all, "details", region, gameId] as const, +}; + +export interface GameData { + commonTitle: string; + cover: string | null; + description: string; + developer: string; + discs: number; + genre: string; + id: string | string[]; + languages: string[]; + officialTitle: string; + publisher: string; + region: string; + releaseDate: string; + title: string; +} + +function mapRegionToApi(region: string): string { + switch (region.toLowerCase()) { + case "america": + case "usa": + case "ntsc-u": + return "America"; + case "europe": + case "pal": + case "ntsc-pal": + return "Europe"; + case "japan": + case "ntsc-j": + return "Japan"; + default: + return "America"; + } +} + +export async function fetchGameData( + region: string, + gameId: string +): Promise { + const apiRegion = mapRegionToApi(region); + const response = await fetch( + `https://psxdata.barrenechea.cl/${apiRegion}/${gameId}.json` + ); + + if (!response.ok) { + throw new Error("Failed to fetch game data"); + } + + const data = (await response.json()) as GameData; + return { + ...data, + cover: data.cover + ? `https://psxdata.barrenechea.cl/${apiRegion}/covers/${gameId}.${data.cover.split(".").pop()}` + : null, + }; +} diff --git a/src/main.tsx b/src/main.tsx index 8f33e3b..976340a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,11 +1,13 @@ import "./index.css"; +import { QueryClientProvider } from "@tanstack/react-query"; import { createRouter, RouterProvider } from "@tanstack/react-router"; import { ThemeProvider } from "next-themes"; import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; import { LoadingDialogProvider } from "@/contexts/loading-dialog-context"; +import { queryClient } from "@/lib/query"; // Import the generated route tree import { routeTree } from "./routeTree.gen"; @@ -26,11 +28,13 @@ if (!rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement); root.render( - - - - - + + + + + + + ); } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index e3d3334..4289b45 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -3,6 +3,15 @@ import { lazy, Suspense } from "react"; import { Sidebar } from "@/components/sidebar"; +const ReactQueryDevtools = import.meta.env.PROD + ? () => null // Render nothing in production + : lazy(() => + // Lazy load in development + import("@tanstack/react-query-devtools").then((res) => ({ + default: res.ReactQueryDevtools, + })) + ); + const TanStackRouterDevtools = import.meta.env.PROD ? () => null // Render nothing in production : lazy(() => @@ -25,7 +34,8 @@ export const Route = createRootRoute({ - + + ),