Next.js客户端渲染CSR #

一、CSR基础 #

1.1 什么是CSR #

客户端渲染是在浏览器中渲染页面:

text
请求 → 返回空HTML → 加载JS → 浏览器渲染

1.2 CSR优点 #

  • 交互响应快
  • 页面切换流畅
  • 服务器压力小

1.3 CSR缺点 #

  • SEO不友好
  • 首屏加载慢
  • 需要JS支持

1.4 适用场景 #

  • 管理后台
  • 用户仪表盘
  • 交互丰富的应用
  • 需要登录的页面

二、客户端组件 #

2.1 声明客户端组件 #

tsx
'use client'

import { useState } from 'react'

export default function Counter() {
    const [count, setCount] = useState(0)
    
    return (
        <button onClick={() => setCount(count + 1)}>
            点击次数: {count}
        </button>
    )
}

2.2 何时使用客户端组件 #

场景 说明
交互功能 onClick, onChange等
状态管理 useState, useReducer
生命周期 useEffect
浏览器API window, document
自定义Hooks 封装逻辑

2.3 客户端组件限制 #

  • 不能直接访问后端资源
  • 不能使用async组件
  • 不能使用服务端特性

三、状态管理 #

3.1 useState #

tsx
'use client'

import { useState } from 'react'

export default function TodoList() {
    const [todos, setTodos] = useState<string[]>([])
    const [input, setInput] = useState('')
    
    const addTodo = () => {
        setTodos([...todos, input])
        setInput('')
    }
    
    return (
        <div>
            <input
                value={input}
                onChange={(e) => setInput(e.target.value)}
            />
            <button onClick={addTodo}>添加</button>
            <ul>
                {todos.map((todo, index) => (
                    <li key={index}>{todo}</li>
                ))}
            </ul>
        </div>
    )
}

3.2 useReducer #

tsx
'use client'

import { useReducer } from 'react'

type State = { count: number }
type Action = { type: 'increment' } | { type: 'decrement' }

function reducer(state: State, action: Action): State {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 }
        case 'decrement':
            return { count: state.count - 1 }
        default:
            return state
    }
}

export default function Counter() {
    const [state, dispatch] = useReducer(reducer, { count: 0 })
    
    return (
        <div>
            <p>计数: {state.count}</p>
            <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
            <button onClick={() => dispatch({ type: 'increment' })}>+</button>
        </div>
    )
}

3.3 Context #

tsx
'use client'

import { createContext, useContext, useState } from 'react'

interface ThemeContextType {
    theme: string
    setTheme: (theme: string) => void
}

const ThemeContext = createContext<ThemeContextType | null>(null)

export function useTheme() {
    const context = useContext(ThemeContext)
    if (!context) {
        throw new Error('useTheme must be used within ThemeProvider')
    }
    return context
}

export function ThemeProvider({ children }: { children: React.ReactNode }) {
    const [theme, setTheme] = useState('light')
    
    return (
        <ThemeContext.Provider value={{ theme, setTheme }}>
            {children}
        </ThemeContext.Provider>
    )
}

四、生命周期 #

4.1 useEffect #

tsx
'use client'

import { useState, useEffect } from 'react'

export default function UserProfile({ userId }: { userId: string }) {
    const [user, setUser] = useState<User | null>(null)
    const [loading, setLoading] = useState(true)
    
    useEffect(() => {
        fetch(`/api/users/${userId}`)
            .then(res => res.json())
            .then(data => {
                setUser(data)
                setLoading(false)
            })
    }, [userId])
    
    if (loading) return <div>加载中...</div>
    
    return <div>{user?.name}</div>
}

4.2 useLayoutEffect #

tsx
'use client'

import { useLayoutEffect, useRef } from 'react'

export default function MeasureElement() {
    const ref = useRef<HTMLDivElement>(null)
    
    useLayoutEffect(() => {
        if (ref.current) {
            console.log('元素尺寸:', ref.current.offsetWidth, ref.current.offsetHeight)
        }
    }, [])
    
    return <div ref={ref}>内容</div>
}

4.3 清理副作用 #

tsx
'use client'

import { useState, useEffect } from 'react'

export default function Timer() {
    const [seconds, setSeconds] = useState(0)
    
    useEffect(() => {
        const interval = setInterval(() => {
            setSeconds(s => s + 1)
        }, 1000)
        
        return () => clearInterval(interval)
    }, [])
    
    return <div>计时: {seconds}秒</div>
}

五、动态导入 #

5.1 基本用法 #

tsx
import dynamic from 'next/dynamic'

const HeavyComponent = dynamic(() => import('./HeavyComponent'))

export default function Page() {
    return (
        <div>
            <h1>页面标题</h1>
            <HeavyComponent />
        </div>
    )
}

5.2 加载状态 #

tsx
import dynamic from 'next/dynamic'

const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
    loading: () => <p>加载中...</p>,
})

export default function Page() {
    return <HeavyComponent />
}

5.3 禁用SSR #

tsx
import dynamic from 'next/dynamic'

const Chart = dynamic(() => import('./Chart'), {
    ssr: false,
})

export default function Page() {
    return <Chart />
}

5.4 命名导出 #

tsx
import dynamic from 'next/dynamic'

const NamedComponent = dynamic(
    () => import('./Components').then(mod => mod.NamedComponent)
)

六、事件处理 #

6.1 基本事件 #

tsx
'use client'

export default function Form() {
    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault()
        console.log('表单提交')
    }
    
    const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
        console.log('按钮点击')
    }
    
    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        console.log('输入值:', e.target.value)
    }
    
    return (
        <form onSubmit={handleSubmit}>
            <input onChange={handleChange} />
            <button type="submit" onClick={handleClick}>
                提交
            </button>
        </form>
    )
}

6.2 键盘事件 #

tsx
'use client'

import { useState } from 'react'

export default function SearchInput() {
    const [query, setQuery] = useState('')
    
    const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
        if (e.key === 'Enter') {
            console.log('搜索:', query)
        }
    }
    
    return (
        <input
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            onKeyDown={handleKeyDown}
            placeholder="按回车搜索"
        />
    )
}

6.3 自定义事件 #

tsx
'use client'

import { useEffect, useRef } from 'react'

export default function CustomEvent() {
    const ref = useRef<HTMLDivElement>(null)
    
    useEffect(() => {
        const element = ref.current
        
        const handleCustomEvent = (e: CustomEvent) => {
            console.log('自定义事件:', e.detail)
        }
        
        element?.addEventListener('customEvent', handleCustomEvent as EventListener)
        
        return () => {
            element?.removeEventListener('customEvent', handleCustomEvent as EventListener)
        }
    }, [])
    
    return <div ref={ref}>自定义事件示例</div>
}

七、浏览器API #

7.1 localStorage #

tsx
'use client'

import { useState, useEffect } from 'react'

export default function PersistentCounter() {
    const [count, setCount] = useState(0)
    
    useEffect(() => {
        const saved = localStorage.getItem('count')
        if (saved) {
            setCount(parseInt(saved, 10))
        }
    }, [])
    
    useEffect(() => {
        localStorage.setItem('count', count.toString())
    }, [count])
    
    return (
        <div>
            <p>计数: {count}</p>
            <button onClick={() => setCount(count + 1)}>增加</button>
        </div>
    )
}

7.2 window对象 #

tsx
'use client'

import { useState, useEffect } from 'react'

export default function WindowSize() {
    const [size, setSize] = useState({ width: 0, height: 0 })
    
    useEffect(() => {
        const handleResize = () => {
            setSize({
                width: window.innerWidth,
                height: window.innerHeight,
            })
        }
        
        handleResize()
        window.addEventListener('resize', handleResize)
        
        return () => window.removeEventListener('resize', handleResize)
    }, [])
    
    return (
        <div>
            窗口尺寸: {size.width} x {size.height}
        </div>
    )
}

7.3 IntersectionObserver #

tsx
'use client'

import { useEffect, useRef, useState } from 'react'

export default function LazyLoad({ children }: { children: React.ReactNode }) {
    const ref = useRef<HTMLDivElement>(null)
    const [isVisible, setIsVisible] = useState(false)
    
    useEffect(() => {
        const observer = new IntersectionObserver(
            ([entry]) => {
                if (entry.isIntersecting) {
                    setIsVisible(true)
                    observer.disconnect()
                }
            },
            { threshold: 0.1 }
        )
        
        if (ref.current) {
            observer.observe(ref.current)
        }
        
        return () => observer.disconnect()
    }, [])
    
    return (
        <div ref={ref}>
            {isVisible ? children : <div>加载中...</div>}
        </div>
    )
}

八、最佳实践 #

8.1 服务端与客户端分离 #

tsx
export default async function Page() {
    const data = await getData()
    
    return (
        <div>
            <StaticHeader data={data.header} />
            <ClientInteractive />
        </div>
    )
}

8.2 减少客户端组件 #

tsx
export default async function Page() {
    const posts = await getPosts()
    
    return (
        <ul>
            {posts.map(post => (
                <li key={post.id}>
                    <PostCard post={post} />
                </li>
            ))}
        </ul>
    )
}

function PostCard({ post }: { post: Post }) {
    return <div>{post.title}</div>
}

8.3 懒加载交互组件 #

tsx
import dynamic from 'next/dynamic'

const InteractiveChart = dynamic(() => import('./Chart'), {
    loading: () => <ChartSkeleton />,
})

export default function Page() {
    return (
        <div>
            <StaticContent />
            <InteractiveChart />
        </div>
    )
}

九、总结 #

CSR要点:

要点 说明
客户端组件 ‘use client’
状态管理 useState/useReducer
生命周期 useEffect
动态导入 dynamic()
事件处理 React事件系统
浏览器API window/localStorage

下一步,让我们学习样式处理!

最后更新:2026-03-28