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