4
Next.js + shadcn/ui + React Query Development Guide
Full-stack development guidelines for Next.js 15 with App Router, TypeScript, Tailwind CSS, shadcn/ui components, and TanStack React Query for data fetching.
MMarie Dubois
·Nov 22, 2025·32 views# CLAUDE.md - Next.js + TypeScript + Tailwind + shadcn + React Query
## Tech Stack
- **Language**: TypeScript (^5.0.0)
- **Framework**: Next.js (App Router)
- **Styling**: Tailwind CSS
- **Component Library**: shadcn/ui
- **Data Fetching**: React Query (TanStack Query)
- **Testing**: Jest + React Testing Library
- **Package Manager**: pnpm
## Development Commands
```bash
pnpm dev # Start development server
pnpm build # Production build
pnpm start # Start production server
pnpm lint # Run ESLint
pnpm format # Run Prettier
pnpm test # Run tests
pnpm test:watch # Run tests in watch mode
```
## Directory Structure
```
src/
├── app/ # Next.js App Router pages
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ └── api/ # API routes
├── components/ # React components
│ ├── ui/ # shadcn/ui components
│ └── [feature]/ # Feature-specific components
├── hooks/ # Custom React hooks
├── lib/ # Utility functions and configurations
│ ├── utils.ts # General utilities
│ └── api.ts # API client
├── types/ # TypeScript type definitions
└── tests/ # Test files
```
## React Query Implementation
### Query Client Setup
Initialize QueryClient in the root layout:
```typescript
// app/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
refetchOnWindowFocus: false,
},
},
}))
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
```
### Query Key Patterns
Use domain-prefixed query keys:
```typescript
// hooks/use-users.ts
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: Filters) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
}
```
### Query Hooks
```typescript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// Fetch query
export function useUser(id: string) {
return useQuery({
queryKey: userKeys.detail(id),
queryFn: () => fetchUser(id),
enabled: !!id,
})
}
// Mutation with cache invalidation
export function useUpdateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updateUser,
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: userKeys.detail(data.id) })
queryClient.invalidateQueries({ queryKey: userKeys.lists() })
},
})
}
// Infinite query for pagination
export function useInfiniteUsers(filters: Filters) {
return useInfiniteQuery({
queryKey: userKeys.list(filters),
queryFn: ({ pageParam = 0 }) => fetchUsers({ ...filters, offset: pageParam }),
getNextPageParam: (lastPage) => lastPage.nextOffset,
})
}
```
## shadcn/ui Usage
Use shadcn/ui components by default for form elements, cards, dialogs, etc.
### Component Installation
```bash
pnpm dlx shadcn@latest add button
pnpm dlx shadcn@latest add card
pnpm dlx shadcn@latest add dialog
pnpm dlx shadcn@latest add form
```
### Component Usage
```typescript
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
export function UserCard({ user }: { user: User }) {
return (
<Card>
<CardHeader>
<CardTitle>{user.name}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">{user.email}</p>
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
Edit
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit User</DialogTitle>
</DialogHeader>
{/* Form content */}
</DialogContent>
</Dialog>
</CardContent>
</Card>
)
}
```
## Code Standards
### TypeScript
- Use explicit return types on functions
- Avoid `any` - prefer `unknown` with type guards
- Use interfaces for object shapes, types for unions/primitives
- Prefer `const` assertions for literal types
### React
- Use arrow functions for components
- Destructure props in function signature
- Co-locate component-specific hooks
- Use `'use client'` only when necessary
### Import Order
1. React and Next.js imports
2. Third-party library imports
3. Local component imports
4. Local utility imports
5. Type imports
```typescript
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useQuery } from '@tanstack/react-query'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import type { User } from '@/types'
```
### Styling
- Use Tailwind CSS utilities for styling
- Use `cn()` helper for conditional classes
- Follow mobile-first responsive design
- Use CSS variables for theming
```typescript
import { cn } from '@/lib/utils'
<div className={cn(
'flex items-center gap-4 p-4',
'rounded-lg border bg-card',
isActive && 'border-primary',
className
)}>
```
## Error Handling
```typescript
// API error handling
export async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.statusText}`)
}
return response.json()
}
// Component error handling
export function UserProfile({ id }: { id: string }) {
const { data, error, isLoading } = useUser(id)
if (isLoading) return <Skeleton className="h-32 w-full" />
if (error) return <ErrorMessage error={error} />
if (!data) return null
return <UserCard user={data} />
}
```
## Testing
```typescript
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
return ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
describe('UserCard', () => {
it('displays user information', async () => {
render(<UserCard user={mockUser} />, { wrapper: createWrapper() })
expect(screen.getByText(mockUser.name)).toBeInTheDocument()
expect(screen.getByText(mockUser.email)).toBeInTheDocument()
})
it('opens edit dialog on button click', async () => {
const user = userEvent.setup()
render(<UserCard user={mockUser} />, { wrapper: createWrapper() })
await user.click(screen.getByRole('button', { name: /edit/i }))
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
})
})
```
Comments
No comments yet
Be the first to share your thoughts!