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