Source:
ocean/docs/patterns/data-fetching.md| ✏️ Edit on GitHub
Data Fetching Patterns
Ocean provides sophisticated data fetching patterns that handle loading states, errors, and caching automatically.
Core Hooks
useGraphQLQuery
Type-safe GraphQL queries with automatic error handling and performance monitoring.
import { useGraphQLQuery } from '@/hooks/use-graphql-query'
// Basic usage
const { data, isLoading, error } = useGraphQLQuery(
GET_USER_QUERY,
{ userId: user.id },
{
enabled: !!user.id,
staleTime: 5 * 60 * 1000, // 5 minutes
}
)
// With typed response
interface UserData {
user: {
id: string
name: string
email: string
}
}
const { data } = useGraphQLQuery<UserData>(GET_USER_QUERY, variables)
useGraphQLMutation
Type-safe mutations with built-in error handling.
import { useGraphQLMutation } from '@/hooks/use-graphql-mutation'
const mutation = useGraphQLMutation(UPDATE_USER_MUTATION)
const handleUpdate = async (data: UpdateUserInput) => {
const result = await mutation.mutateAsync({
userId: user.id,
input: data,
})
// Result is typed based on your GraphQL schema
}
useMutationWithToast
The recommended pattern for all mutations. Automatically handles:
- Loading states
- Success/error toast notifications
- Query cache invalidation
- Error logging
import { useMutationWithToast } from '@/hooks/use-mutation-with-toast'
const updateProfile = useMutationWithToast({
mutationFn: async (data: ProfileUpdate) => {
const response = await fetch('/api/profile', {
method: 'PUT',
body: JSON.stringify(data)
})
if (!response.ok) throw new Error('Update failed')
return response.json()
},
successMessage: 'Profile updated successfully',
errorMessage: 'Failed to update profile', // Optional, auto-generated if omitted
invalidateQueries: [['profile', userId]], // Refresh these queries on success
onSuccess: (data) => {
// Optional callback
console.log('Updated:', data)
}
})
// Usage in component
<Button
onClick={() => updateProfile.mutate(formData)}
disabled={updateProfile.isPending}
>
{updateProfile.isPending ? 'Saving...' : 'Save Profile'}
</Button>
API Client Hook
useApiClient
Provides a unified interface for all API interactions.
const apiClient = useApiClient()
// GraphQL operations
const userData = await apiClient.graphql(GET_USER_QUERY, { userId })
const result = await apiClient.graphqlMutation(UPDATE_USER_MUTATION, { input })
// Supabase operations
const { data, error } = await apiClient.supabase.from('profiles').select('*').eq('id', userId)
// Edge function calls
const response = await apiClient.edgeFunction('stripe-checkout', {
priceId: 'price_123',
})
Retry Logic
withRetry
Built-in exponential backoff for resilient API calls.
import { withRetry } from '@/lib/api-utils'
// Basic usage
const data = await withRetry(() => fetch('/api/flaky-endpoint'), {
maxAttempts: 3,
delayMs: 1000,
backoffFactor: 2, // Exponential backoff
})
// With custom error handling
const result = await withRetry(
async () => {
const res = await fetch('/api/data')
if (res.status === 429) {
throw new Error('Rate limited') // Will retry
}
if (res.status === 404) {
return null // Won't retry, returns immediately
}
return res.json()
},
{
shouldRetry: (error, attempt) => {
// Custom retry logic
return attempt < 3 && error.message !== 'Not found'
},
}
)
Query Key Patterns
Consistent query key patterns for cache management:
// User queries
;['user', userId][('users', { organizationId, page, limit })][
// Organization queries
('organization', orgId)
][('organizations', userId)][
// Combined queries
('organization-members', orgId)
][('user-permissions', userId, orgId)]
// Invalidation examples
queryClient.invalidateQueries({ queryKey: ['user'] }) // All user queries
queryClient.invalidateQueries({ queryKey: ['user', userId] }) // Specific user
Loading States
Skeleton Components
Pre-built loading skeletons for consistent UX:
import { DashboardLoadingSkeleton } from '@/components/dashboard-loading-skeleton'
import { AuthLoadingSkeleton } from '@/components/auth-loading-skeleton'
// In your component
if (isLoading) {
return <DashboardLoadingSkeleton />
}
// Custom skeleton pattern
function MyComponentSkeleton() {
return (
<div className="space-y-4">
<Skeleton className="h-8 w-64" /> {/* Title */}
<Skeleton className="h-4 w-full" /> {/* Text line */}
<Skeleton className="h-4 w-3/4" /> {/* Text line */}
</div>
)
}
Error Handling
Queries and mutations integrate with error boundaries:
// Errors are automatically caught by error boundaries
const { data } = useGraphQLQuery(QUERY, variables, {
throwOnError: true // Default: false
})
// Manual error handling
const { data, error } = useGraphQLQuery(QUERY, variables)
if (error) {
return <ErrorMessage error={error} />
}
Performance Monitoring
All GraphQL operations are automatically monitored:
// Automatic performance tracking includes:
// - Operation name
// - Duration
// - Success/failure status
// - Variable size
// - Response size
// Custom performance marks
performance.mark('my-operation-start')
const result = await apiCall()
performance.mark('my-operation-end')
performance.measure('my-operation', 'my-operation-start', 'my-operation-end')
Best Practices
- Always use
useMutationWithToastfor mutations that modify data - Set appropriate
staleTimefor queries based on data volatility - Use consistent query keys for effective cache invalidation
- Implement loading skeletons for better perceived performance
- Let error boundaries handle errors instead of manual try-catch
- Use
enabledoption to prevent unnecessary queries - Batch related queries when possible for efficiency
Common Patterns
Dependent Queries
const { data: user } = useGraphQLQuery(GET_USER, { id: userId })
const { data: posts } = useGraphQLQuery(
GET_USER_POSTS,
{ userId: user?.id },
{ enabled: !!user?.id } // Only run when user is loaded
)
Optimistic Updates
const mutation = useMutationWithToast({
mutationFn: updatePost,
onMutate: async (newData) => {
// Cancel in-flight queries
await queryClient.cancelQueries({ queryKey: ['post', postId] })
// Save current data
const previousPost = queryClient.getQueryData(['post', postId])
// Optimistically update
queryClient.setQueryData(['post', postId], newData)
return { previousPost }
},
onError: (err, newData, context) => {
// Rollback on error
queryClient.setQueryData(['post', postId], context.previousPost)
},
})
Pagination
const [page, setPage] = useState(1)
const { data, isPreviousData } = useGraphQLQuery(
GET_POSTS,
{ page, limit: 20 },
{
keepPreviousData: true, // Smooth pagination
}
)