Frontend Development
Modern React development with TypeScript, Tailwind CSS, Next.js, and the TanStack ecosystem.
Project Setup
Initialize Next.js Project
npx create-next-app@latest my-app --typescript --tailwind --app --src-dir --import-alias "@/*"
cd my-app
npm install @tanstack/react-query @tanstack/react-router @tanstack/start
Essential Configuration
tsconfig.json:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"baseUrl": ".",
"paths": { "@/*": ["./src/*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
tailwind.config.ts:
import type { Config } from "tailwindcss";
const config: Config = {
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
},
},
},
plugins: [],
};
export default config;
app/globals.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
}
TanStack Router
File-based Routing Structure
src/
├── routes/
│ ├── index.tsx # /
│ ├── about.tsx # /about
│ ├── posts/
│ │ ├── index.tsx # /posts
│ │ ├── [$postId].tsx # /posts/:postId
│ │ └── new.tsx # /posts/new
└── app/
├── layout.tsx
└── page.tsx
Router Configuration
router.tsx:
import { createRootRoute, createRouter, Outlet } from '@tanstack/react-router'
import { NotFoundComponent } from './components/not-found'
const rootRoute = createRootRoute({
component: () => (
<div className="min-h-screen bg-background text-foreground">
<header className="border-b p-4">
<h1 className="text-2xl font-bold">My App</h1>
</header>
<main className="p-4">
<Outlet />
</main>
</div>
),
notFoundComponent: NotFoundComponent,
})
const router = createRouter({ routeTree: rootRoute })
declare module '@tanstack/react-router' {
interface Register { router: typeof router }
}
export default router
main.tsx:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { RouterProvider } from '@tanstack/react-router'
import router from './router'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)
Route with Loader
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const postSchema = z.object({
id: z.string(),
title: z.string(),
content: z.string(),
createdAt: z.string(),
})
export const Route = createFileRoute('/posts/$postId')({
validateSearch: z.object({ filter: z.string().optional() }),
loader: async ({ params, search }) => {
const res = await fetch(`/api/posts/${params.postId}?filter=${search.filter}`)
return postSchema.parse(await res.json())
},
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
const { post } = Route.useLoaderData()
return (
<div className="p-6">
<h1 className="text-3xl font-bold mb-4">{post.title}</h1>
<p className="text-gray-600">{post.content}</p>
</div>
)
}
Layout Route
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard/_layout')({
component: DashboardLayout,
})
function DashboardLayout() {
return (
<div className="dashboard-layout">
<nav className="p-4 border-b">Dashboard Navigation</nav>
<Outlet />
</div>
)
}
Error Boundary
export function NotFoundComponent({ error }: { error: Error }) {
return (
<div className="p-6 text-center">
<h2 className="text-2xl font-bold">404</h2>
<p className="text-gray-600">Page not found</p>
</div>
)
}
TanStack Query
Query Client Setup
query-client.ts:
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
retry: 1,
},
},
});
app/layout.tsx:
import { QueryClientProvider } from '@tanstack/react-query'
import { queryClient } from '../query-client'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<div className="min-h-screen bg-background text-foreground">
{children}
</div>
</QueryClientProvider>
)
}
Basic Data Fetching
import { useQuery } from '@tanstack/react-query'
function PostList() {
const { data: posts, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const res = await fetch('/api/posts')
return res.json()
},
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div className="space-y-4">
{posts.map((post: any) => (
<div key={post.id} className="border p-4 rounded">
{post.title}
</div>
))}
</div>
)
}
Optimistic Updates
import { useMutation, useQueryClient } from '@tanstack/react-query'
function PostForm() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: async (newPost: any) => {
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
})
return res.json()
},
onMutate: async (newPost) => {
await queryClient.cancelQueries({ queryKey: ['posts'] })
const previousPosts = queryClient.getQueryData(['posts'])
queryClient.setQueryData(['posts'], (old: any[] = []) => [
...old,
{ ...newPost, id: Date.now().toString() },
])
return { previousPosts }
},
onError: (err, newPost, context) => {
if (context?.previousPosts) {
queryClient.setQueryData(['posts'], context.previousPosts)
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] })
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement)
mutation.mutate({
title: formData.get('title'),
content: formData.get('content'),
})
}}
className="space-y-4"
>
<input name="title" placeholder="Title" className="border p-2 w-full" />
<textarea name="content" placeholder="Content" className="border p-2 w-full" />
<button
type="submit"
disabled={mutation.isPending}
className="bg-blue-600 text-white px-4 py-2 rounded"
>
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
</form>
)
}
Pagination
import { useQuery } from '@tanstack/react-query'
function PaginatedPostList() {
const [page, setPage] = useState(1)
const { data, isLoading } = useQuery({
queryKey: ['posts', page],
queryFn: async () => {
const res = await fetch(`/api/posts?page=${page}`)
return res.json()
},
keepPreviousData: true,
})
return (
<div>
{isLoading && <div>Loading...</div>}
<div className="space-y-4">
{data?.data?.map((post: any) => (
<div key={post.id} className="border p-4 rounded">{post.title}</div>
))}
</div>
<div className="flex justify-between mt-4">
<button onClick={() => setPage(p => p - 1)} disabled={page === 1} className="border px-4 py-2 rounded">
Previous
</button>
<span>Page {page}</span>
<button onClick={() => setPage(p => p + 1)} disabled={!data?.hasNext} className="border px-4 py-2 rounded">
Next
</button>
</div>
</div>
)
}
TanStack Start
Server-side Data Loading
import { createFileRoute, json } from '@tanstack/start'
export const Route = createFileRoute('/dashboard/$userId')({
loader: async ({ params }) => {
const res = await fetch(`https://api.example.com/users/${params.userId}`)
if (!res.ok) throw new Error('User not found')
const userData = await res.json()
return json({
user: {
id: userData.id,
name: userData.name,
email: userData.email,
},
})
},
component: DashboardPage,
})
function DashboardPage() {
const { user } = Route.useLoaderData()
return (
<div className="p-6">
<h1 className="text-2xl font-bold">Welcome, {user.name}!</h1>
<p className="text-gray-600">{user.email}</p>
</div>
)
}
Server Actions
import { createFileRoute, ServerFunction } from '@tanstack/start'
const createPost: ServerFunction = async ({ request }) => {
const formData = await request.formData()
const title = formData.get('title') as string
const content = formData.get('content') as string
if (!title || !content) {
return { error: 'Title and content are required' }
}
const newPost = await db.post.create({ data: { title, content } })
return { success: true, post: newPost }
}
export const Route = createFileRoute('/posts/new')({
component: NewPostPage,
})
function NewPostPage() {
const navigate = useNavigate()
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement)
const result = await createPost({ request: new Request('', { method: 'POST', body: formData }) })
if (!result.error) {
navigate({ to: `/posts/${result.post.id}` })
}
}
return <form onSubmit={onSubmit}>{/* form fields */}</form>
}
Component Patterns
Utility Functions
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
import React, { forwardRef } from 'react'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
const Slot = forwardRef<any, React.HTMLAttributes<HTMLElement>>(
({ children, ...props }, ref) => {
const child = React.Children.only(children) as React.ReactElement
return React.cloneElement(child, {
...props,
...child.props,
ref,
})
}
)
Slot.displayName = 'Slot'
Button Component
import { cva, type VariantProps } from 'class-variance-authority'
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
size?: 'default' | 'sm' | 'lg' | 'icon'
asChild?: boolean
}
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-blue-600 text-white hover:bg-blue-700',
destructive: 'bg-red-600 text-white hover:bg-red-700',
outline: 'border border-gray-300 bg-white hover:bg-gray-100 text-gray-800',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
ghost: 'hover:bg-gray-100',
link: 'text-blue-600 underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: { variant: 'default', size: 'default' },
}
)
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'default', size = 'default', asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = 'Button'
Input Component
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white',
'placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
'disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = 'Input'
Card Compound Component
const Card = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('rounded-lg border bg-white shadow-sm', className)} {...props} />
)
)
Card.displayName = 'Card'
const CardHeader = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
)
)
CardHeader.displayName = 'CardHeader'
const CardTitle = forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} />
)
)
CardTitle.displayName = 'CardTitle'
const CardDescription = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn('text-sm text-gray-600', className)} {...props} />
)
)
CardDescription.displayName = 'CardDescription'
const CardContent = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
)
)
CardContent.displayName = 'CardContent'
const CardFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
)
)
CardFooter.displayName = 'CardFooter'
Dialog Compound Component
type DialogContextValue = {
open: boolean
onOpenChange: (open: boolean) => void
}
const DialogContext = React.createContext<DialogContextValue | undefined>(undefined)
function useDialog() {
const context = React.useContext(DialogContext)
if (!context) throw new Error('Dialog components must be used within a Dialog')
return context
}
interface DialogProps {
open?: boolean
onOpenChange?: (open: boolean) => void
children: React.ReactNode
}
function Dialog({ open = false, onOpenChange, children }: DialogProps) {
return (
<DialogContext.Provider value={{ open, onOpenChange: onOpenChange ?? (() => {}) }}>
{children}
</DialogContext.Provider>
)
}
Dialog.displayName = 'Dialog'
const DialogTrigger = forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
({ asChild = false, ...props }, ref) => {
const { onOpenChange } = useDialog()
const Comp = asChild ? Slot : 'button'
return <Comp ref={ref} onClick={() => onOpenChange(true)} {...props} />
}
)
DialogTrigger.displayName = 'DialogTrigger'
const DialogContent = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, children, ...props }, ref) => {
const { open, onOpenChange } = useDialog()
if (!open) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={() => onOpenChange(false)} />
<div
ref={ref}
className={cn(
'relative z-50 w-full max-w-md rounded-lg bg-white p-6 shadow-lg animate-in fade-in-0 zoom-in-95 duration-200',
className
)}
{...props}
>
{children}
</div>
</div>
)
}
)
DialogContent.displayName = 'DialogContent'
const DialogHeader = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
)
)
DialogHeader.displayName = 'DialogHeader'
const DialogTitle = forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h2 ref={ref} className={cn('text-lg font-semibold leading-none tracking-tight', className)} {...props} />
)
)
DialogTitle.displayName = 'DialogTitle'
const DialogDescription = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn('text-sm text-gray-600', className)} {...props} />
)
)
DialogDescription.displayName = 'DialogDescription'
const DialogFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...props} />
)
)
DialogFooter.displayName = 'DialogFooter'
const DialogClose = forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
({ className, ...props }, ref) => {
const { onOpenChange } = useDialog()
return (
<button ref={ref} className={cn('rounded-sm opacity-70 hover:opacity-100', className)} onClick={() => onOpenChange(false)} {...props} />
)
}
)
DialogClose.displayName = 'DialogClose'
Usage Examples
import { useState } from 'react'
// Button usage
<Button variant="default">Click me</Button>
<Button variant="destructive" size="sm">Delete</Button>
<Button variant="outline">Cancel</Button>
<Button variant="ghost">Ghost</Button>
<Button asChild>
<a href="/login">Login with Link</a>
</Button>
// Input usage
<Input type="email" placeholder="Email" />
<Input type="password" placeholder="Password" />
// Card usage
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description goes here</CardDescription>
</CardHeader>
<CardContent>
<p>Card content goes here</p>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</CardFooter>
</Card>
// Dialog usage
function MyDialog() {
const [open, setOpen] = useState(false)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>This action cannot be undone. This will permanently delete your account.</DialogDescription>
</DialogHeader>
<div className="py-4">
<p>Some dialog content here</p>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button variant="destructive">Delete Account</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
Composition Patterns
// 1. forwardRef pattern - for ref forwarding
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
ref={ref}
className={cn('flex h-10 w-full rounded-md border bg-white px-3 py-2', className)}
{...props}
/>
)
}
)
// 2. asChild pattern - polymorphic components
function Button({ asChild = false, children, ...props }: ButtonProps) {
const Comp = asChild ? Slot : 'button'
return <Comp {...props}>{children}</Comp>
}
// Usage: Button wraps Link
<Button asChild>
<Link href="/dashboard">Dashboard</Link>
</Button>
// 3. Context pattern - for compound components
const TabsContext = React.createContext<{ value: string } | undefined>(undefined)
function Tabs({ value, children }: { value: string; children: React.ReactNode }) {
return <TabsContext.Provider value={{ value }}>{children}</TabsContext.Provider>
}
function TabList({ children }: { children: React.ReactNode }) {
return <div className="flex border-b">{children}</div>
}
function Tab({ value, children }: { value: string; children: React.ReactNode }) {
const context = React.useContext(TabsContext)
const isActive = context?.value === value
return (
<button className={cn('px-4 py-2', isActive && 'border-b-2 border-blue-600')}>
{children}
</button>
)
}
// Usage: compound components
<Tabs value="tab1">
<TabList>
<Tab value="tab1">Tab 1</Tab>
<Tab value="tab2">Tab 2</Tab>
</TabList>
</Tabs>
// 4. cva pattern - for variant management
import { cva, type VariantProps } from 'class-variance-authority'
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors',
{
variants: {
variant: {
default: 'border-transparent bg-blue-600 text-white',
secondary: 'border-transparent bg-gray-200 text-gray-800',
destructive: 'border-transparent bg-red-600 text-white',
outline: 'text-gray-800 border-gray-300',
},
},
defaultVariants: {
variant: 'default',
},
}
)
interface BadgeProps extends VariantProps<typeof badgeVariants> {}
function Badge({ variant, className, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
}
// Usage
<Badge variant="secondary">Badge</Badge>
<Badge variant="destructive">Error</Badge>
Data Fetching Component
import { useQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const res = await fetch(`/api/posts/${params.postId}`)
return res.json()
},
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
const { data: post, isLoading, error } = useQuery({
queryKey: ['post', postId],
queryFn: async () => {
const res = await fetch(`/api/posts/${postId}`)
return res.json()
},
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div className="p-6">
<h1 className="text-3xl font-bold mb-4">{post.title}</h1>
<p className="text-gray-600">{post.content}</p>
</div>
)
}
Form Component
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
password: z.string().min(6),
})
export function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
})
const onSubmit = (data: any) => {
// Handle form submission
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<input {...register('email')} className="w-full px-3 py-2 border rounded" />
{errors.email && <p className="text-red-500 text-sm">{errors.email.message}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">Password</label>
<input {...register('password')} type="password" className="w-full px-3 py-2 border rounded" />
{errors.password && <p className="text-red-500 text-sm">{errors.password.message}</p>}
</div>
<button type="submit" className="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700">
Login
</button>
</form>
)
}
TypeScript Utility Types
// Common types
type ApiResponse<T> = { data: T; message?: string; error?: string }
type Status = 'loading' | 'success' | 'error'
// Type guards
function isUser(data: unknown): data is User {
return typeof data === 'object' && data !== null && 'id' in data && 'name' in data
}
// Generic components
interface GenericListProps<T> {
items: T[]
renderItem: (item: T, index: number) => React.ReactNode
className?: string
}
function GenericList<T>({ items, renderItem, className }: GenericListProps<T>) {
return <div className={className}>{items.map((item, i) => renderItem(item, i))}</div>
}
// Query types
type PostQuery = { id: string; title: string; content: string; createdAt: string }
// Event types
type FormEvent = React.FormEvent<HTMLFormElement>
type ChangeEvent = React.ChangeEvent<HTMLInputElement>
Tailwind Utility Classes
// Common component classes
const buttonStyles = {
primary: "bg-blue-600 text-white hover:bg-blue-700 px-4 py-2 rounded",
secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300 px-4 py-2 rounded",
destructive: "bg-red-600 text-white hover:bg-red-700 px-4 py-2 rounded",
outline: "border border-gray-300 px-4 py-2 rounded hover:bg-gray-50",
ghost: "px-4 py-2 rounded hover:bg-gray-100",
};
const inputStyles = {
base: "w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500",
error: "border-red-500 focus:ring-red-500",
};
const cardStyles = {
base: "border rounded-lg p-6 shadow-sm",
hover: "hover:shadow-md transition-shadow",
};
Common Workflows
Protected Routes
export const Route = createFileRoute("/dashboard/_protected")({
beforeLoad: ({ location }) => {
if (!isAuthenticated()) {
throw redirect({ to: "/login", search: { redirect: location.pathname } });
}
},
component: DashboardPage,
});
function isAuthenticated(): boolean {
return !!localStorage.getItem("auth-token");
}
API Error Handling
try {
const res = await fetch("/api/data");
if (!res.ok) {
const error = await res.json();
throw new Error(error.message || "Request failed");
}
return res.json();
} catch (error) {
console.error("API Error:", error);
throw error;
}
Loading States
function DataComponent() {
const { data, isLoading, isFetching } = useQuery({ queryKey: ['data'], queryFn: fetchData })
if (isLoading) return <div className="animate-pulse">Loading...</div>
if (isFetching) <div>Updating...</div>
return <div>{data}</div>
}