Next.js Client Components数据获取 #
一、客户端数据获取概述 #
1.1 适用场景 #
| 场景 | 说明 |
|---|---|
| 用户交互 | 搜索、筛选、排序 |
| 实时数据 | 轮询更新 |
| 私有数据 | 需要用户认证 |
| 渐进增强 | 先展示静态内容,再加载动态 |
1.2 客户端组件声明 #
tsx
'use client'
import { useState, useEffect } from 'react'
export default function ClientComponent() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData)
}, [])
return <div>{JSON.stringify(data)}</div>
}
二、使用SWR #
2.1 安装SWR #
bash
npm install swr
2.2 基本用法 #
tsx
'use client'
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then(res => res.json())
export default function UserList() {
const { data, error, isLoading } = useSWR('/api/users', fetcher)
if (isLoading) return <div>加载中...</div>
if (error) return <div>加载失败</div>
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
2.3 全局配置 #
tsx
'use client'
import { SWRConfig } from 'swr'
export default function Provider({ children }) {
return (
<SWRConfig
value={{
fetcher: (url: string) => fetch(url).then(res => res.json()),
revalidateOnFocus: false,
dedupingInterval: 60000,
}}
>
{children}
</SWRConfig>
)
}
2.4 条件获取 #
tsx
'use client'
import useSWR from 'swr'
export default function UserProfile({ userId }: { userId: string | null }) {
const { data } = useSWR(userId ? `/api/users/${userId}` : null)
if (!userId) return <div>请选择用户</div>
return <div>{data?.name}</div>
}
2.5 变更与重新验证 #
tsx
'use client'
import useSWR, { useSWRConfig } from 'swr'
export default function UserList() {
const { data, mutate } = useSWR('/api/users')
const { mutate: globalMutate } = useSWRConfig()
const addUser = async (name: string) => {
const newUser = { name }
await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser),
})
mutate()
}
return (
<div>
<button onClick={() => addUser('New User')}>
添加用户
</button>
<ul>
{data?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
2.6 乐观更新 #
tsx
'use client'
import useSWR from 'swr'
export default function TodoList() {
const { data, mutate } = useSWR('/api/todos')
const toggleTodo = async (id: string, completed: boolean) => {
mutate(
data.map(todo =>
todo.id === id ? { ...todo, completed } : todo
),
false
)
await fetch(`/api/todos/${id}`, {
method: 'PATCH',
body: JSON.stringify({ completed }),
})
mutate()
}
return (
<ul>
{data?.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id, !todo.completed)}
/>
{todo.title}
</li>
))}
</ul>
)
}
三、使用React Query #
3.1 安装 #
bash
npm install @tanstack/react-query
3.2 基本配置 #
tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
export default function Providers({ children }) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
3.3 查询数据 #
tsx
'use client'
import { useQuery } from '@tanstack/react-query'
async function fetchUsers() {
const res = await fetch('/api/users')
return res.json()
}
export default function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
if (isLoading) return <div>加载中...</div>
if (error) return <div>加载失败</div>
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
3.4 变更数据 #
tsx
'use client'
import { useMutation, useQueryClient } from '@tanstack/react-query'
async function createUser(name: string) {
const res = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ name }),
})
return res.json()
}
export default function CreateUserForm() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
mutation.mutate(formData.get('name') as string)
}
return (
<form onSubmit={handleSubmit}>
<input name="name" />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? '创建中...' : '创建用户'}
</button>
</form>
)
}
3.5 查询参数 #
tsx
'use client'
import { useQuery } from '@tanstack/react-query'
export default function SearchResults({ query }: { query: string }) {
const { data, isLoading } = useQuery({
queryKey: ['search', query],
queryFn: () => fetch(`/api/search?q=${query}`).then(res => res.json()),
enabled: query.length > 0,
staleTime: 60000,
})
if (isLoading) return <div>搜索中...</div>
return (
<ul>
{data?.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
)
}
四、原生fetch #
4.1 useEffect获取 #
tsx
'use client'
import { useState, useEffect } from 'react'
export default function UserList() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [])
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error}</div>
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
4.2 自定义Hook #
tsx
'use client'
import { useState, useEffect } from 'react'
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
const controller = new AbortController()
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
})
.then(data => {
setData(data)
setLoading(false)
})
.catch(err => {
if (err.name !== 'AbortError') {
setError(err)
setLoading(false)
}
})
return () => controller.abort()
}, [url])
return { data, loading, error }
}
export default function UserList() {
const { data: users, loading, error } = useFetch<User[]>('/api/users')
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error.message}</div>
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
五、表单处理 #
5.1 表单提交 #
tsx
'use client'
import { useState } from 'react'
export default function ContactForm() {
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setStatus('loading')
const formData = new FormData(e.currentTarget)
try {
const res = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(Object.fromEntries(formData)),
headers: { 'Content-Type': 'application/json' },
})
if (res.ok) {
setStatus('success')
} else {
setStatus('error')
}
} catch {
setStatus('error')
}
}
return (
<form onSubmit={handleSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit" disabled={status === 'loading'}>
{status === 'loading' ? '发送中...' : '发送'}
</button>
{status === 'success' && <p>发送成功!</p>}
{status === 'error' && <p>发送失败,请重试</p>}
</form>
)
}
5.2 文件上传 #
tsx
'use client'
import { useState } from 'react'
export default function FileUpload() {
const [uploading, setUploading] = useState(false)
const [progress, setProgress] = useState(0)
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setUploading(true)
const formData = new FormData()
formData.append('file', file)
try {
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
if (res.ok) {
alert('上传成功')
}
} finally {
setUploading(false)
}
}
return (
<div>
<input type="file" onChange={handleUpload} disabled={uploading} />
{uploading && <div>上传中...</div>}
</div>
)
}
六、实时数据 #
6.1 轮询 #
tsx
'use client'
import useSWR from 'swr'
export default function LiveStats() {
const { data } = useSWR('/api/stats', {
refreshInterval: 5000,
})
return (
<div>
<p>在线用户: {data?.onlineUsers}</p>
<p>今日访问: {data?.todayVisits}</p>
</div>
)
}
6.2 WebSocket #
tsx
'use client'
import { useState, useEffect } from 'react'
export default function LiveChat() {
const [messages, setMessages] = useState<Message[]>([])
useEffect(() => {
const ws = new WebSocket('wss://api.example.com/chat')
ws.onmessage = (event) => {
const message = JSON.parse(event.data)
setMessages(prev => [...prev, message])
}
return () => ws.close()
}, [])
return (
<div>
{messages.map(msg => (
<div key={msg.id}>{msg.text}</div>
))}
</div>
)
}
七、最佳实践 #
7.1 混合渲染 #
tsx
import { Suspense } from 'react'
import ClientData from './ClientData'
export default function Page() {
return (
<div>
<h1>静态内容</h1>
<Suspense fallback={<div>加载中...</div>}>
<ClientData />
</Suspense>
</div>
)
}
7.2 错误边界 #
tsx
'use client'
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div>
<p>出错了: {error.message}</p>
<button onClick={resetErrorBoundary}>重试</button>
</div>
)
}
export default function SafeComponent({ children }) {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
{children}
</ErrorBoundary>
)
}
7.3 加载状态 #
tsx
'use client'
import useSWR from 'swr'
export default function UserList() {
const { data, isLoading, error } = useSWR('/api/users')
if (isLoading) {
return (
<div className="animate-pulse space-y-2">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-4 bg-gray-200 rounded w-3/4"></div>
))}
</div>
)
}
if (error) return <div>加载失败</div>
return (
<ul>
{data?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
八、总结 #
Client Components数据获取要点:
| 要点 | 说明 |
|---|---|
| SWR | 轻量级数据获取 |
| React Query | 功能丰富的数据获取 |
| 原生fetch | 简单场景 |
| 表单处理 | 表单提交和文件上传 |
| 实时数据 | 轮询和WebSocket |
| 错误处理 | ErrorBoundary |
下一步,让我们学习缓存策略!
最后更新:2026-03-28