feat: add React Query setup with api helper
- Install @tanstack/react-query - Create QueryClientProvider in ClientProviders with stable client instance - Add queryClient.ts with createQueryClient() and api() helper - Add api() helper that wraps fetch with automatic /api prefix - Add example.ts with complete CRUD hook examples - Configure sensible defaults (5min staleTime, retry once) All API routes are now prefixed with /api automatically via api() helper.
This commit is contained in:
@@ -45,6 +45,7 @@
|
||||
"@soroban/core": "workspace:*",
|
||||
"@soroban/templates": "workspace:*",
|
||||
"@tanstack/react-form": "^0.19.0",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"emojibase-data": "^16.0.3",
|
||||
"lucide-react": "^0.294.0",
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
import { ReactNode, useState } from 'react'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { AbacusDisplayProvider } from '@soroban/abacus-react'
|
||||
import { UserProfileProvider } from '@/contexts/UserProfileContext'
|
||||
import { GameModeProvider } from '@/contexts/GameModeContext'
|
||||
import { FullscreenProvider } from '@/contexts/FullscreenContext'
|
||||
import { DeploymentInfo } from './DeploymentInfo'
|
||||
import { createQueryClient } from '@/lib/queryClient'
|
||||
|
||||
interface ClientProvidersProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function ClientProviders({ children }: ClientProvidersProps) {
|
||||
// Create a stable QueryClient instance that persists across renders
|
||||
const [queryClient] = useState(() => createQueryClient())
|
||||
|
||||
return (
|
||||
<AbacusDisplayProvider>
|
||||
<UserProfileProvider>
|
||||
<GameModeProvider>
|
||||
<FullscreenProvider>
|
||||
{children}
|
||||
<DeploymentInfo />
|
||||
</FullscreenProvider>
|
||||
</GameModeProvider>
|
||||
</UserProfileProvider>
|
||||
</AbacusDisplayProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AbacusDisplayProvider>
|
||||
<UserProfileProvider>
|
||||
<GameModeProvider>
|
||||
<FullscreenProvider>
|
||||
{children}
|
||||
<DeploymentInfo />
|
||||
</FullscreenProvider>
|
||||
</GameModeProvider>
|
||||
</UserProfileProvider>
|
||||
</AbacusDisplayProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
126
apps/web/src/lib/api/example.ts
Normal file
126
apps/web/src/lib/api/example.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Example API hooks using React Query
|
||||
*
|
||||
* This file demonstrates how to use React Query with the configured
|
||||
* QueryClient and apiUrl helper for making API requests.
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/queryClient'
|
||||
|
||||
// Example type for an API resource
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Example query hook - fetches a list of users
|
||||
*/
|
||||
export function useUsers() {
|
||||
return useQuery({
|
||||
queryKey: ['users'],
|
||||
queryFn: async () => {
|
||||
const response = await api('users')
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch users')
|
||||
}
|
||||
return response.json() as Promise<User[]>
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Example query hook - fetches a single user by ID
|
||||
*/
|
||||
export function useUser(userId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['users', userId],
|
||||
queryFn: async () => {
|
||||
const response = await api(`users/${userId}`)
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch user')
|
||||
}
|
||||
return response.json() as Promise<User>
|
||||
},
|
||||
enabled: !!userId, // Only run query if userId is provided
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Example mutation hook - creates a new user
|
||||
*/
|
||||
export function useCreateUser() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (newUser: Omit<User, 'id'>) => {
|
||||
const response = await api('users', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(newUser),
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create user')
|
||||
}
|
||||
return response.json() as Promise<User>
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate and refetch users query after successful creation
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Example mutation hook - updates a user
|
||||
*/
|
||||
export function useUpdateUser() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (user: User) => {
|
||||
const response = await api(`users/${user.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(user),
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update user')
|
||||
}
|
||||
return response.json() as Promise<User>
|
||||
},
|
||||
onSuccess: (updatedUser) => {
|
||||
// Invalidate both the list and the individual user query
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['users', updatedUser.id] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Example mutation hook - deletes a user
|
||||
*/
|
||||
export function useDeleteUser() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (userId: string) => {
|
||||
const response = await api(`users/${userId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete user')
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate users query after successful deletion
|
||||
queryClient.invalidateQueries({ queryKey: ['users'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
67
apps/web/src/lib/queryClient.ts
Normal file
67
apps/web/src/lib/queryClient.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { QueryClient, DefaultOptions } from '@tanstack/react-query'
|
||||
|
||||
const queryConfig: DefaultOptions = {
|
||||
queries: {
|
||||
// Default stale time of 5 minutes
|
||||
staleTime: 1000 * 60 * 5,
|
||||
// Retry failed requests once
|
||||
retry: 1,
|
||||
// Refetch on window focus in production only
|
||||
refetchOnWindowFocus: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new QueryClient instance with default configuration.
|
||||
* All API routes are expected to be prefixed with /api.
|
||||
*/
|
||||
export function createQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: queryConfig,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to construct API URLs with the /api prefix.
|
||||
* Use this for consistency when making API calls.
|
||||
*
|
||||
* @param path - The API path without the /api prefix (e.g., 'users', 'posts/123')
|
||||
* @returns The full API URL (e.g., '/api/users', '/api/posts/123')
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const url = apiUrl('users') // '/api/users'
|
||||
* const url = apiUrl('posts/123') // '/api/posts/123'
|
||||
* ```
|
||||
*/
|
||||
export function apiUrl(path: string): string {
|
||||
// Remove leading slash if present to avoid double slashes
|
||||
const cleanPath = path.startsWith('/') ? path.slice(1) : path
|
||||
return `/api/${cleanPath}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around fetch that automatically prefixes paths with /api.
|
||||
* Provides a consistent way to make API calls throughout the application.
|
||||
*
|
||||
* @param path - The API path without the /api prefix (e.g., 'users', 'posts/123')
|
||||
* @param options - Standard fetch options (method, headers, body, etc.)
|
||||
* @returns Promise with the fetch Response
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // GET request
|
||||
* const response = await api('users')
|
||||
* const users = await response.json()
|
||||
*
|
||||
* // POST request
|
||||
* const response = await api('users', {
|
||||
* method: 'POST',
|
||||
* headers: { 'Content-Type': 'application/json' },
|
||||
* body: JSON.stringify({ name: 'John' })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function api(path: string, options?: RequestInit): Promise<Response> {
|
||||
return fetch(apiUrl(path), options)
|
||||
}
|
||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -130,6 +130,9 @@ importers:
|
||||
'@tanstack/react-form':
|
||||
specifier: ^0.19.0
|
||||
version: 0.19.0(@types/react@18.2.0)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.90.2
|
||||
version: 5.90.2(react@18.2.0)
|
||||
'@types/jsdom':
|
||||
specifier: ^21.1.7
|
||||
version: 21.1.7
|
||||
@@ -6362,6 +6365,10 @@ packages:
|
||||
'@tanstack/store': 0.3.1
|
||||
dev: false
|
||||
|
||||
/@tanstack/query-core@5.90.2:
|
||||
resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==}
|
||||
dev: false
|
||||
|
||||
/@tanstack/react-form@0.19.0(@types/react@18.2.0)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-9cE+ic8iHN8UtR4k65286qwDk1pF3ESHiLw3aWDUySezduz4K61Jl3CkwDbuMUnUvzymoSlqWm0Lppqzj3ASng==}
|
||||
peerDependencies:
|
||||
@@ -6377,6 +6384,15 @@ packages:
|
||||
- react-dom
|
||||
dev: false
|
||||
|
||||
/@tanstack/react-query@5.90.2(react@18.2.0):
|
||||
resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19
|
||||
dependencies:
|
||||
'@tanstack/query-core': 5.90.2
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@tanstack/react-store@0.3.1(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-PfV271d345It6FdcX4c9gd+llKGddtvau8iJnybTAWmYVyDeFWfIIkiAJ5iNITJmI02AzqgtcV3QLNBBlpBUjA==}
|
||||
peerDependencies:
|
||||
|
||||
Reference in New Issue
Block a user