Install
openclaw skills install react-advanced掌握 React Server Components、Suspense、useTransition、渲染优化、自定义 hook 设计和 Next.js App Router 最佳实践。
openclaw skills install react-advanced掌握 React 并发特性、Server Components、渲染优化和 Next.js App Router 架构,写出真正高性能的 React 应用。
| 用户描述 | 实际问题 | 第一步 |
|---|---|---|
| 「RSC 怎么用」「Server Component」 | RSC 概念和边界 | 解释 RSC/CC 边界,给出决策树 |
| 「重渲染太多」「性能差」 | 渲染优化 | 先用 React DevTools Profiler 定位,再给方案 |
| 「memo 没用」「useMemo 没效果」 | 优化工具误用 | 检查引用稳定性,找真正的重渲染原因 |
| 「Suspense 怎么用」 | 异步渲染 | 区分数据加载 Suspense 和代码分割 Suspense |
| 「hook 怎么设计」 | 抽象和复用 | 问:要解决什么问题?是状态逻辑还��副作用? |
这个组件需要以下任意一项吗?
├── useState / useReducer
├── useEffect / useLayoutEffect
├── 浏览器 API(window、document、localStorage)
├── 事件监听(onClick、onChange)
├── 第三方需要浏览器环境的库
│
├── 是 → Client Component(文件顶部加 'use client')
└── 否 → Server Component(默认,不需要标注)
├── 可以直接 async/await 获取数据
├── 可以访问数据库、文件系统、环境变量
└── 不会打包进客户端 bundle
RSC 的核心价值:
常见误区:
// ❌ 错:在 Server Component 里用 useState
// 这会报错,Server Component 不能有状态
async function ServerComp() {
const [count, setCount] = useState(0) // 报错
}
// ✅ 对:状态放在 Client Component,数据获取放在 Server Component
async function ServerComp() {
const data = await fetchData() // 服务端获取
return <ClientComp initialData={data} /> // 传给客户端
}
'use client'
function ClientComp({ initialData }) {
const [data, setData] = useState(initialData) // 客户端状态
}
'use client' 是边界,不是标签:
'use client' 的文件及其所有子组件都变成 Client Component'use client' 推到叶子节点,让更多组件保持 Server Component两种用途,不要混淆:
用途 1:代码分割(懒加载组件)
const HeavyChart = lazy(() => import('./HeavyChart'))
function Dashboard() {
return (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
)
}
用途 2:数据加载(配合 React Query 或 RSC)
// React Query + Suspense
function UserProfile({ userId }) {
// useSuspenseQuery 在数据未就绪时抛出 Promise,触发最近的 Suspense
const { data } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
return <div>{data.name}</div>
}
// 父组件
function App() {
return (
<Suspense fallback={<Skeleton />}>
<ErrorBoundary fallback={<Error />}>
<UserProfile userId={1} />
</ErrorBoundary>
</Suspense>
)
}
Suspense 边界的粒度:
useTransition: 把状态更新标记为"非紧急",让 React 优先处理用户输入。
function SearchPage() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
function handleSearch(e) {
setQuery(e.target.value) // 紧急:立即更新输入框
startTransition(() => {
setResults(heavyFilter(e.target.value)) // 非紧急:可以被打断
})
}
return (
<>
<input value={query} onChange={handleSearch} />
{isPending ? <Spinner /> : <ResultList results={results} />}
</>
)
}
useDeferredValue: 延迟某个值的更新,适合不能修改子组件的场景。
function SearchPage() {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query) // 延迟跟随 query
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<Suspense fallback={<Spinner />}>
<SearchResults query={deferredQuery} /> {/* 用延迟值 */}
</Suspense>
</>
)
}
什么时候用哪个:
useTransitionuseDeferredValue先用工具定位,不要凭感觉优化:
memo 不生效的常见原因:
// ❌ 每次渲染都创建新对象,memo 无效
function Parent() {
return <Child style={{ color: 'red' }} /> // 每次新对象
}
// ✅ 提到组件外或用 useMemo
const style = { color: 'red' } // 组件外,引用稳定
function Parent() {
return <Child style={style} />
}
// ❌ 每次渲染都创建新函数,memo 无效
function Parent() {
return <Child onClick={() => doSomething()} />
}
// ✅ 用 useCallback
function Parent() {
const handleClick = useCallback(() => doSomething(), [])
return <Child onClick={handleClick} />
}
memo 的正确使用时机:
Context 导致的过度渲染:
// ❌ 任何 context 值变化,所有消费者都重渲染
const AppContext = createContext({ user, theme, notifications })
// ✅ 拆分 context,按更新频率分组
const UserContext = createContext(user) // 很少变
const ThemeContext = createContext(theme) // 偶尔变
const NotifContext = createContext(notifs) // 频繁变
好的自定义 hook 的特征:
设计模式:
// ❌ 只是打包了几个 state,没有封装关注点
function useForm() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
return { name, setName, email, setEmail }
}
// ✅ 封装了表单的完整行为
function useForm(schema, onSubmit) {
const [values, setValues] = useState({})
const [errors, setErrors] = useState({})
const [isSubmitting, setIsSubmitting] = useState(false)
const setValue = useCallback((field, value) => {
setValues(v => ({ ...v, [field]: value }))
// 清除该字段的错误
setErrors(e => ({ ...e, [field]: undefined }))
}, [])
const submit = useCallback(async () => {
const result = schema.safeParse(values)
if (!result.success) {
setErrors(result.error.flatten().fieldErrors)
return
}
setIsSubmitting(true)
try { await onSubmit(result.data) }
finally { setIsSubmitting(false) }
}, [values, schema, onSubmit])
return { values, errors, isSubmitting, setValue, submit }
}
hook 的命名规范:
useXxx:返回状态和操作(useModal、useForm)useXxxQuery:封装数据获取(useUserQuery)useXxxStore:封装 store 订阅(useCartStore)| 场景 | 方案 | 原因 |
|---|---|---|
| 静态内容(博客、文档) | RSC + fetch 默认缓存 | 构建时生成,CDN 缓存 |
| 动态内容(用户数据) | RSC + fetch + cache: 'no-store' | 每次请求都获取最新数据 |
| 需要实时更新 | Client Component + React Query | 客户端轮询或 WebSocket |
| 表单提交 | Server Actions | 无需 API 路由,直接操作数据库 |
Server Actions(表单提交):
// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
await db.post.create({ data: { title } })
revalidatePath('/posts') // 重新验证缓存
}
// app/new-post/page.tsx
import { createPost } from '../actions'
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">发布</button>
</form>
)
}
| 反模式 | 问题 | 正确做法 |
|---|---|---|
所有组件加 'use client' | 失去 RSC 的所有优势 | 只在需要交互的叶子节点加 |
| useEffect 做数据获取 | 竞态条件、闪烁、重复请求 | 用 React Query 或 RSC |
| 默认给所有组件加 memo | 过早优化,增加代码复杂度 | 先 profile,再优化 |
| Context 存高频更新的状态 | 全局重渲染 | 高频状态用 Zustand |
| 在 useEffect 里更新 state 触发另一个 useEffect | 依赖链难以追踪 | 合并逻辑或用 useReducer |