Client Concepts
Fully typed React hooks powered by TanStack Query
Client Concepts
Fully Typed React Hooks
Corsair integrates with TanStack Query to provide fully typed React hooks for every query and mutation you define.
const { data, isLoading, error } = useCorsairQuery(
'all published posts with comments by author id',
{ authorId: 'abc123' }
)
// data is fully typed:
// Array<{
// id: string;
// title: string;
// content: string;
// publishedAt: Date;
// comments: Array<{ id: string; content: string; createdAt: Date; }>
// }>What you get from TanStack:
- Automatic caching
- Background refetching
- Query invalidation
- Loading and error states
- Optimistic updates
All with complete type safety.
Client and Server Components
Corsair works seamlessly with both client and server components.
Server Components
// app/posts/page.tsx
export default async function PostsPage() {
const posts = await useCorsairQuery('all published posts with authors')
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
)
}Client Components
'use client'
// app/posts/interactive.tsx
export function InteractivePosts() {
const { data: posts, isLoading } = useCorsairQuery(
'all published posts with authors'
)
if (isLoading) return <Spinner />
return (
<div>
{posts?.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
)
}Mutations with Optimistic Updates
'use client'
export function CreatePostButton() {
const createPost = useCorsairMutation('create post with title and content')
const handleCreate = async () => {
await createPost.mutate({
title: 'New Post',
content: 'Hello world',
})
}
return (
<button onClick={handleCreate} disabled={createPost.isPending}>
{createPost.isPending ? 'Creating...' : 'Create Post'}
</button>
)
}Query Invalidation
When you mutate data, Corsair integrates with TanStack's invalidation system to keep your UI in sync.
'use client'
export function DeletePostButton({ postId }: { postId: string }) {
const queryClient = useQueryClient()
const deletePost = useCorsairMutation('delete post by id')
const handleDelete = async () => {
await deletePost.mutate({ postId })
// Invalidate and refetch posts
queryClient.invalidateQueries({
queryKey: ['corsair', 'all published posts with authors'],
})
}
return <button onClick={handleDelete}>Delete</button>
}Automatic cache management:
- Invalidate specific queries after mutations
- Refetch data in the background
- Update UI automatically when data changes
Type Exports for Component Props
Corsair exports types for every query and mutation, making it easy to build type-safe component interfaces.
The QueryOutputs Type
import { QueryOutputs } from '@/corsair/types'
interface ArtistCardProps {
artist: QueryOutputs['get all artists'][number]
onClick?: (artist: QueryOutputs['get all artists'][number]) => void
}
export function ArtistCard({ artist, onClick }: ArtistCardProps) {
return (
<div onClick={() => onClick?.(artist)}>
<h3>{artist.name}</h3>
<p>{artist.genre}</p>
</div>
)
}The MutationInputs Type
import { MutationInputs } from '@/corsair/types'
interface CreateUserFormProps {
onSubmit: (
data: MutationInputs['create user with email and password']
) => void
}
export function CreateUserForm({ onSubmit }: CreateUserFormProps) {
// Form implementation with properly typed inputs
}Why this matters:
- Build components with exact types from your queries
- No manual type definitions needed
- Changes to queries automatically update component props
- Works with arrays using
[number]indexing
Full-Stack Type Safety
In the event you're using a separate backend and frontend, Corsair passes types from the API to the client—just like tRPC does.
How It Works
- You define a query on your backend
- Corsair generates the implementation and types
- The types are automatically available on your frontend
- A small change in one place ripples through your entire stack
Example: Separate Backend and Frontend
Backend (api/src/queries.ts):
export const getUser = useCorsairQuery(
'user with profile and recent orders by id',
{ userId: string }
)Frontend (web/src/components/UserProfile.tsx):
import { QueryOutputs } from '@/corsair/types'
interface UserProfileProps {
user: QueryOutputs['user with profile and recent orders by id']
}
export function UserProfile({ user }: UserProfileProps) {
return (
<div>
<h1>{user.profile.name}</h1>
<p>Recent orders: {user.recentOrders.length}</p>
</div>
)
}What happens when you change the query:
- Update the backend query to include
user.email - TypeScript immediately shows errors in the frontend
- Fix the type errors—your app stays in sync
- No manual type syncing required
Caching Strategies
Corsair leverages TanStack Query's powerful caching system.
Stale-While-Revalidate
const { data } = useCorsairQuery(
'user dashboard stats',
{ userId },
{
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
}
)Polling for Real-Time Data
const { data } = useCorsairQuery(
'active orders pending fulfillment',
{},
{
refetchInterval: 10000, // Poll every 10 seconds
}
)Manual Refetching
const { data, refetch } = useCorsairQuery('current inventory levels')
return <button onClick={() => refetch()}>Refresh Inventory</button>The Goal
Corsair's client integration provides:
- Fully typed React hooks powered by TanStack Query
- Works with both client and server components
- Automatic caching and background refetching
- Type exports for building type-safe component props
- Full-stack type safety that syncs backend and frontend
- Query invalidation to keep your UI in sync with data changes
You get the developer experience of tRPC with the flexibility of natural language queries.