React 入门 —— 给 Vue / Angular 老兵的速成文档
React 入门 —— 给 Vue / Angular 老兵的速成文档
默认读者:JS/TS 熟练,写过多年 Vue(包括 Composition API)和 Angular。
目标:用最少的概念跑通 React 的心智模型 + 直接能写业务页。
风格:对照 Vue / Angular 来记,避免重新学一遍 UI 套路。
0. React 是什么(一句话版)
- 不是框架,只是个「把 state 变成 UI」的视图库。
- 路由、请求、状态管理、表单、表格……全靠生态拼。
- 核心公式:
UI = f(state)
state 一变 → 组件函数整体重跑 → React 拿新旧 JSX diff → 更新真实 DOM。
记住这条,后面所有「为什么这样写」都能解释。
1. 三大框架心智模型对照
| 维度 | Vue 3 | Angular | React |
|---|---|---|---|
| 组件载体 | .vue 单文件 |
.ts + .html + .css |
一个函数(.tsx) |
| 模板 | template + 指令 | template + 指令 | JSX(JS 表达式) |
| 响应式 | ref / reactive 自动追踪 |
RxJS / Signals / Zone | 不自动,必须 setState |
| 父传子 | props |
@Input() |
props(函数参数) |
| 子传父 | emit |
@Output() EventEmitter |
传函数 prop(onXxx) |
| 双向绑定 | v-model |
[(ngModel)] |
没有,自己写 value + onChange |
| 条件渲染 | v-if |
*ngIf |
{cond && <X/>} 或三元 |
| 列表渲染 | v-for |
*ngFor |
array.map(...) + key |
| 计算属性 | computed |
getter / computed() signal |
useMemo(多数时候用变量就行) |
| 监听 | watch |
RxJS / effect() |
useEffect(fn, [deps]) |
| 生命周期 | onMounted 等 |
ngOnInit 等 |
useEffect(统一) |
| DI | provide/inject | Injector(强 DI) | Context / 直接 import |
| 路由 | vue-router | @angular/router | react-router-dom(第三方) |
| 状态管理 | Pinia | NgRx / Signals | Zustand / Redux Toolkit / Context |
最大的心智差异:Vue/Angular 的响应式是「数据变 → 框架知道」;
React 是「你告诉 React 数据变了」,必须调setXxx,否则不渲染。
2. 项目起步(10 分钟)
npm create vite@latest my-app -- --template react-ts
cd my-app
npm i
npm run dev
目录:
src/
main.tsx # 入口,挂载 <App/>
App.tsx # 根组件
components/ # 自己拆
main.tsx 长这样:
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
3. JSX —— 在 JS 里写 HTML 的规则
function Demo() {
const name = '小明'
const list = [1, 2, 3]
const visible = true
return (
<div className="box" style={{ color: 'red' }}>
<h1>你好 {name}</h1>
{visible && <p>会显示</p>}
{visible ? <p>A</p> : <p>B</p>}
<ul>
{list.map((n) => (
<li key={n}>{n}</li>
))}
</ul>
</div>
)
}
规则要点
| Vue 写法 | JSX 写法 |
|---|---|
class="x" |
className="x" |
:class="{a: ok}" |
className={ok ? 'a' : ''} |
style="color: red" |
style={{ color: 'red' }}(对象) |
@click="fn" |
onClick={fn} |
v-if |
{cond && <X/>} |
v-for |
arr.map(x => <X key={x.id}/>) |
{{ x }} |
{x} |
| 多根节点 | 用 <>...</>(Fragment) |
key必须、稳定、唯一。别用 index(数据顺序会变就 GG)。
4. 组件 = 函数
type Props = {
title: string
onClose: () => void // 子→父:传函数
children?: React.ReactNode // 默认插槽
}
export default function Modal({ title, onClose, children }: Props) {
return (
<div className="modal">
<header>{title} <button onClick={onClose}>×</button></header>
<main>{children}</main>
</div>
)
}
用:
<Modal title="提示" onClose={() => setOpen(false)}>
内容
</Modal>
Vue → React 对照
| Vue | React |
|---|---|
defineProps<{...}>() |
函数参数 + TS 类型 |
defineEmits |
传 onXxx 函数 prop |
| 默认 slot | children |
| 具名 slot | 传 ReactNode 类型的 prop,比如 header?: React.ReactNode |
5. 状态:useState
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
)
}
关键规则(新手 90% 的坑)
// ❌ 直接改对象/数组,React 看不到变化
state.list.push(item)
setState(state)
// ✅ 新引用
setState({ ...state, list: [...state.list, item] })
// ❌ 连续调用基于旧值
setCount(count + 1)
setCount(count + 1) // 还是 +1,不是 +2
// ✅ 用函数形式
setCount(c => c + 1)
setCount(c => c + 1)
// ❌ 在渲染里直接 set,会死循环
setCount(count + 1)
// ✅ 放事件、effect 里
对比
| Vue | React |
|---|---|
const count = ref(0); count.value++ |
const [count, setCount] = useState(0); setCount(c=>c+1) |
reactive({a:1}) |
useState({a:1}) + 整体替换 |
| 自动 deep 响应 | 没有 deep,对象/数组要新引用 |
6. 副作用:useEffect(Vue watch + 生命周期 二合一)
import { useEffect, useState } from 'react'
function User({ id }: { id: number }) {
const [data, setData] = useState<any>(null)
useEffect(() => {
let cancelled = false
fetch(`/api/user/${id}`)
.then(r => r.json())
.then(d => { if (!cancelled) setData(d) })
return () => { cancelled = true } // 清理(相当于 onUnmounted / watch stop)
}, [id]) // 依赖变了重跑
return <div>{data?.name}</div>
}
三种依赖写法
| 依赖 | 含义 | 类比 |
|---|---|---|
[] |
只在挂载时跑一次 | onMounted |
[a, b] |
a 或 b 变了再跑 | watch([a,b], ...) |
| 不传 | 每次渲染都跑 | 几乎不要这么写 |
关键坑
- 依赖必须诚实。effect 里用到的外部变量,都要进数组(用 ESLint 的
react-hooks/exhaustive-deps)。 - 请求要带取消标志(上面
cancelled),否则旧请求迟到会覆盖新数据。 - 别拿 effect 当 watch 用所有场景:能在事件里直接做的事,就别放 effect。
- Strict Mode 下开发期 effect 会跑两次,是故意的,逼你写清理函数。
7. 派生数据:useMemo / useCallback
const filtered = useMemo(
() => list.filter(x => x.name.includes(keyword)),
[list, keyword]
)
const handleClick = useCallback((id: number) => {
doSomething(id)
}, [])
useMemo≈ Vue 的computed,但不强制用。
90% 的派生值直接const x = a + b就够了,性能瓶颈再优化。useCallback用于「把函数传给子组件」时保持引用稳定,避免子组件白白重渲。
没传给子组件、没进依赖数组的函数,别套,纯增加复杂度。
一句话:先别优化。React 重渲很便宜,profile 之后再上 memo 三件套(
memo / useMemo / useCallback)。
8. 引用与 DOM:useRef
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
return <input ref={inputRef} />
- 拿 DOM:和 Vue 的
ref一模一样。 - 存「不触发渲染的可变值」:定时器 id、上一次的值等。
改ref.current不会触发重渲,这点和 state 相反。
9. 表单:没有 v-model
function Form() {
const [name, setName] = useState('')
const [age, setAge] = useState<number | ''>('')
const submit = (e: React.FormEvent) => {
e.preventDefault()
console.log({ name, age })
}
return (
<form onSubmit={submit}>
<input value={name} onChange={e => setName(e.target.value)} />
<input
type="number"
value={age}
onChange={e => setAge(e.target.value === '' ? '' : Number(e.target.value))}
/>
<button type="submit">提交</button>
</form>
)
}
- 这叫受控组件:
value+onChange全自己接管。 - 复杂表单别手写,用 UI 库自己的 Form(Ant Design / Arco / TDesign)。
Ant Design React 版的Form.useForm()用法和 Vue 的 Form 校验体验差不多。
10. Context(解决 props 一层层传)
type Theme = 'light' | 'dark'
const ThemeCtx = React.createContext<Theme>('light')
function App() {
return (
<ThemeCtx.Provider value="dark">
<Page />
</ThemeCtx.Provider>
)
}
function DeepChild() {
const theme = useContext(ThemeCtx)
return <div>{theme}</div>
}
- 类比:Vue 的
provide/inject、Angular 的 DI(弱化版)。 - 不要用 Context 当全局状态管理,会让所有消费者重渲。状态多就上 Zustand / Redux Toolkit。
11. 自定义 Hook(最爽的复用方式)
逻辑复用不靠 mixin / 高阶组件,靠自定义 hook(一个以 use 开头的普通函数)。
function useLoading(initial = false) {
const [loading, setLoading] = useState(initial)
return { loading, setLoading }
}
function useRequest<T>(fn: () => Promise<T>, deps: any[] = []) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<unknown>(null)
useEffect(() => {
let cancelled = false
setLoading(true)
fn()
.then(d => { if (!cancelled) setData(d) })
.catch(e => { if (!cancelled) setError(e) })
.finally(() => { if (!cancelled) setLoading(false) })
return () => { cancelled = true }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps)
return { data, loading, error }
}
用:
const { data, loading } = useRequest(() => fetch('/api/user').then(r => r.json()))
Vue 的
useXxx组合式函数思想几乎完全一致,迁移无痛。
12. Hooks 的硬规则(违反就报错)
- 只在函数组件 / 自定义 hook 顶层调用。
不能写在if / for / 回调里。 - 顺序必须稳定。React 靠调用顺序认 hook,乱了就全错。
- 不能在普通函数里调用 hook。
ESLint 装 eslint-plugin-react-hooks 自动帮你检查。
13. 路由:react-router-dom(v6)
npm i react-router-dom
import { createBrowserRouter, RouterProvider, Link, useParams, useNavigate } from 'react-router-dom'
const router = createBrowserRouter([
{ path: '/', element: <Home /> },
{ path: '/user/:id', element: <User /> },
])
function Root() {
return <RouterProvider router={router} />
}
function User() {
const { id } = useParams()
const nav = useNavigate()
return (
<>
<div>user {id}</div>
<button onClick={() => nav('/')}>回首页</button>
</>
)
}
| Vue Router | React Router |
|---|---|
<router-link> |
<Link> |
<router-view> |
<Outlet/>(嵌套路由) |
useRoute() |
useParams() / useLocation() |
useRouter() |
useNavigate() |
| 守卫 | 没有内置,自己写 wrapper 组件 / loader |
14. 请求 + 服务端状态:用 TanStack Query(强烈推荐)
npm i @tanstack/react-query
import { useQuery } from '@tanstack/react-query'
function UserList() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
})
if (isLoading) return <div>loading...</div>
if (error) return <div>err</div>
return <ul>{data.map((u: any) => <li key={u.id}>{u.name}</li>)}</ul>
}
- 自动缓存、去重、重试、轮询、依赖刷新。
- 业务页 90% 的「loading / data / error」三件套都不用自己写了。
- Vue 圈对标的是
@tanstack/vue-query,思路一样。
15. 全局状态:先 Zustand,再考虑 Redux Toolkit
npm i zustand
import { create } from 'zustand'
type Store = {
count: number
inc: () => void
}
const useCounter = create<Store>((set) => ({
count: 0,
inc: () => set(s => ({ count: s.count + 1 })),
}))
function A() {
const count = useCounter(s => s.count)
return <div>{count}</div>
}
function B() {
const inc = useCounter(s => s.inc)
return <button onClick={inc}>+</button>
}
- API 简洁、无 boilerplate,Pinia 用户秒上手。
- 团队大 / 需要严格规范 → Redux Toolkit。
16. UI 库选择(业务向)
| 库 | 备注 |
|---|---|
| Ant Design | 后台首选,组件全,文档好 |
| Arco Design | 字节出品,和你 Vue 项目里用的 Arco 体验一致 |
| TDesign | 腾讯出品 |
| MUI | Material 风,国外团队多 |
| shadcn/ui + Tailwind | 现代风潮,复制源码到项目里、完全可控 |
你已经在用 Arco Vue 版,React 版迁移成本最低。
17. 一个完整可对照的例子(你 Vue 项目里那种「弹窗 + 表单」)
Vue(你已有的写法)
<template>
<a-modal visible @cancel="onCancel" @ok="onOk">
<a-form ref="formRef" :model="form" :rules="rules">
<a-form-item label="名称" field="name">
<a-input v-model="form.name" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
const emit = defineEmits(['submit', 'cancel'])
const formRef = ref()
const form = ref({ name: '' })
const rules = reactive({ name: [{ required: true, message: '请输入' }] })
const onCancel = () => emit('cancel')
const onOk = async () => {
const err = await formRef.value.validate()
if (err) return
emit('submit', form.value)
}
</script>
React 对应
import { useState } from 'react'
import { Modal, Form, Input } from 'antd' // 或 @arco-design/web-react
type Props = {
onSubmit: (data: { name: string }) => void
onCancel: () => void
}
export default function AddStage({ onSubmit, onCancel }: Props) {
const [form] = Form.useForm()
const [loading, setLoading] = useState(false)
const handleOk = async () => {
try {
const values = await form.validateFields()
setLoading(true)
await onSubmit(values)
} finally {
setLoading(false)
}
}
return (
<Modal open title="添加" onCancel={onCancel} onOk={handleOk} confirmLoading={loading}>
<Form form={form} layout="vertical">
<Form.Item
label="名称"
name="name"
rules={[{ required: true, message: '请输入' }]}
>
<Input />
</Form.Item>
</Form>
</Modal>
)
}
父用法:
{open && (
<AddStage
onCancel={() => setOpen(false)}
onSubmit={async (data) => {
await api.add(data)
setOpen(false)
}}
/>
)}
对照重点:
- 没有
visible这种 props,通过条件渲染控制存在/销毁(也可以用openprop)。 - 子组件不
emit,父传onSubmit / onCancel函数下来。 - 表单值由 antd
Form托管,几乎和 Vue 的 Arco Form 一样。
18. 工程化常用搭配(业务推荐栈)
Vite + React + TS
react-router-dom 路由
@tanstack/react-query 请求 / 服务端状态
zustand 客户端状态
antd / arco-react UI
react-hook-form + zod (表单不用 UI 库时)
eslint + prettier 规范
19. 给你这位 Vue/Angular 老兵的「避坑清单」
- 响应式不会自动。所有更新走
setState。 - 状态不可变:对象/数组永远换新引用。
- 函数组件每次渲染都重跑:不要在函数体里写副作用、定时器、订阅。
- 依赖数组要诚实:装
react-hooks/exhaustive-deps,别和它对着干。 - Strict Mode 下 effect 跑两次是正常的,别去关。
useEffect不是 watch 的万能替代:能在事件里直接做的,别推到 effect。- 不要用 index 当 key。
- 优化先 profile:
React.memo / useMemo / useCallback是兜底,不是日常。 - 没有全局
this,没有 DI:依赖靠 import + Context。 - 请求别裸写 useEffect:直接上 react-query,省心 80%。
20. 推荐学习路径(3–5 天可上手业务)
Day 1
- 跑通 Vite + React + TS 模板
- 写:计数器、todo 列表、表单受控输入
- 理解
useState不可变更新
Day 2
- 学
useEffect、清理函数、依赖数组 - 用
fetch+useEffect写一个用户列表 - 自定义 hook:
useRequest、useLoading
Day 3
- React Router:列表页 + 详情页
- Antd / Arco:Form、Table、Modal
- 抄一遍你 Vue 项目里
add-stage.vue、detail-info.vue这种页
Day 4
- TanStack Query 替换裸
useEffect请求 - Zustand 写一个全局 user / 权限 store
Day 5
- 复刻一个真实业务页(比如你 worker 仓库里某个列表+弹窗)
- 写完之后回头看本文「避坑清单」,逐条对照
21. 官方 & 高质量文档
- 官方新文档(强烈推荐):https://react.dev
- React Router:https://reactrouter.com
- TanStack Query:https://tanstack.com/query
- Zustand:https://zustand-demo.pmnd.rs
- Ant Design:https://ant.design
- Arco React:https://arco.design/react
22. 一句话总结
Vue 是「自动响应式 + 模板指令」,React 是「不可变 state + 纯函数 UI」。
你已经会的所有前端工程能力(构建、路由、组件拆分、表单校验、请求封装)几乎平移过去,
真正要换的,只是「数据变更怎么告诉视图」这一层。
把这条记牢,剩下的就是查 API + 写业务。
进阶补充
下面四章是写完业务一阵子后会用到的内容。第一次看可以跳过,等踩到问题再回头看。
23. SSR / SSG / RSC(服务端渲染体系)
23.1 几个名词先分清
| 缩写 | 全称 | 在哪里跑 | 类比 |
|---|---|---|---|
| CSR | Client Side Rendering | 浏览器跑 React | Vue SPA 默认模式 |
| SSR | Server Side Rendering | Node 端把组件渲成 HTML 字符串再返回,浏览器再 hydrate | Nuxt SSR / Angular Universal |
| SSG | Static Site Generation | 构建期就把 HTML 生成好 | Nuxt generate / VitePress |
| ISR | Incremental Static Regeneration | SSG + 后台定时再生 | Next 特有 |
| RSC | React Server Components | 组件本身分服务端 / 客户端,服务端组件不打到前端包 | Vue 没有等价物 |
| Streaming | 流式 SSR | 服务端边渲边吐 HTML | — |
23.2 SSR 的核心好处与代价
好处:
- 首屏 HTML 直接可见 → SEO / 首屏更快
- 服务端能直接读数据库 / 内部接口(RSC 尤其友好)
代价:
- 部署要有 Node 运行时(CSR 一个 nginx 就行)
- 状态、
window/document、定时器、第三方库的 SSR 兼容要注意 - 心智复杂度↑:要分清「这段代码哪边跑」
23.3 现在的主流选择
| 框架 | 模式 | 一句话 |
|---|---|---|
| Next.js (App Router) | SSR / SSG / RSC / ISR / Edge | 事实标准,几乎所有 React SSR 项目都用它 |
| Remix / React Router v7 | SSR / loader-action | 数据流靠 loader/action,体验像写后端 |
| Astro | 默认静态,岛屿架构 | 内容站点首选,可混用多框架 |
| Vite + 自定义 SSR | DIY | 不推荐生产使用 |
23.4 Next.js App Router 极简上手
npx create-next-app@latest my-app
app/
page.tsx // 路由 /
about/page.tsx // 路由 /about
layout.tsx // 公共布局
users/[id]/page.tsx // /users/123
api/hello/route.ts // 接口路由
默认是服务端组件(RSC):
// app/users/page.tsx —— 直接 await,没有 useEffect
export default async function UsersPage() {
const res = await fetch('https://api.example.com/users', { cache: 'no-store' })
const users = await res.json()
return (
<ul>
{users.map((u: any) => <li key={u.id}>{u.name}</li>)}
</ul>
)
}
需要状态 / 事件 / 浏览器 API 的组件加 "use client":
'use client'
import { useState } from 'react'
export default function Counter() {
const [n, setN] = useState(0)
return <button onClick={() => setN(n + 1)}>{n}</button>
}
23.5 RSC 心智速记
| 服务端组件(默认) | 客户端组件("use client") |
|---|---|
能 await fetch / 直连 DB |
能用 useState / useEffect / 事件 |
不能用 hook、事件、window |
不能直接 await 顶层 |
| 不打进 client bundle | 会下发到浏览器 |
| 可嵌套客户端组件 | 不能反过来嵌服务端组件(只能通过 children 传) |
23.6 SSR 常见坑
window is not defined:访问 DOM 的代码放进useEffect,或动态 import +ssr: false。- Hydration mismatch:服务端和客户端首次渲染结果不一致(常见于「时间」「随机数」「依赖 localStorage」),用
suppressHydrationWarning或挪到 effect。 - 环境变量:Next 里
NEXT_PUBLIC_前缀才会下发到浏览器。 - 第三方库 SSR 不兼容:用
next/dynamic动态加载。
23.7 你该不该用 SSR?
| 项目类型 | 建议 |
|---|---|
| 你 worker 这种内部后台管理系统 | 不需要 SSR,老老实实 Vite + CSR 就行 |
| 营销官网 / 内容站 | Astro / Next SSG |
| C 端应用、SEO 重要 | Next.js |
| 重交互、登录后才用 | 普通 SPA |
24. 性能优化深入
24.1 第一性原理
React 慢,永远是其中之一:
- 重渲染太多(组件没必要的 re-render)
- 重渲染太重(一次渲染做了大量 JS 计算)
- DOM 太大 / 太深(虚拟 DOM diff 也救不了几万 DOM)
- 请求 / 资源没优化(首屏白屏其实和 React 无关)
先 profile,再优化。打开 React DevTools → Profiler,录一段交互看「flame chart」,谁红优化谁。别凭直觉到处套 useMemo。
24.2 重渲染来源
一个组件什么时候会重渲?
- 它自己的 state / 它订阅的 context 变了
- 它的父组件重渲了(即使 props 没变也会跟着重渲)
注意:props 即使值相同,引用变了也算变。
24.3 React.memo —— 阻断父级带来的重渲
const Child = React.memo(function Child({ user }: { user: User }) {
return <div>{user.name}</div>
})
- 默认浅比较 props;引用没变就跳过。
- 配合
useCallback / useMemo才有意义,否则父每次给你的 props 都是新引用,memo 形同虚设。
function Parent() {
const [count, setCount] = useState(0)
const user = useMemo(() => ({ name: 'A' }), []) // 稳定引用
const onClick = useCallback(() => console.log('x'), []) // 稳定引用
return <Child user={user} onClick={onClick} />
}
24.4 useMemo / useCallback 的真实场景
只在这三种情况下用:
- 计算真的贵(几 ms 以上,或大数据 filter/sort)
- 把值传给 memo 化的子组件(保稳定引用)
- 把函数传给 memo 化的子组件 / 进 effect deps
其它都是过早优化,反而徒增 hook 数量。
24.5 状态拆分 / 状态下移
降低重渲染范围最有效的手段。
// ❌ 整个表单一个大 state,输入一个字段,全部子组件都重渲
const [form, setForm] = useState({ name: '', age: 0, address: '' })
// ✅ 拆开 / 或者把 input 抽成自己管 state 的子组件
function NameField() {
const [name, setName] = useState('')
return <input value={name} onChange={e => setName(e.target.value)} />
}
口诀:state 离用它的组件越近越好。
24.6 Context 性能陷阱
Context 一变,所有消费者都重渲,与是否真正用到那部分值无关。
解决:
- 拆 Context:常变的 / 不常变的分开
- selector 模式:用 zustand / jotai / use-context-selector
- 别拿 Context 当全局 store
24.7 列表性能
| 问题 | 方案 |
|---|---|
| 长列表(>500 项) | 虚拟滚动:react-virtuoso / @tanstack/react-virtual |
| 表格大数据 | AG Grid / TanStack Table + 虚拟化 |
| key 不稳定 | 永远用业务 id,不用 index |
| 每行重渲 | 行组件 React.memo,并传稳定的 props |
24.8 大组件 / 路由级代码分割
import { lazy, Suspense } from 'react'
const Heavy = lazy(() => import('./Heavy'))
<Suspense fallback={<div>loading...</div>}>
<Heavy />
</Suspense>
- 路由用
lazy拆 chunk,按需加载。 - Vite / Next 默认会做 route-based splitting,但页内重组件你要自己拆。
24.9 useTransition(标记非紧急更新)
const [keyword, setKeyword] = useState('')
const [list, setList] = useState<Item[]>([])
const [isPending, startTransition] = useTransition()
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setKeyword(e.target.value) // 紧急:输入框立即更新
startTransition(() => { // 非紧急:大列表过滤
setList(filter(allData, e.target.value))
})
}
输入框打字流畅,列表更新可被中断 / 推迟。
24.10 React 19 的 React Compiler
- 官方自动优化方案,编译期自动加 memo。
- 一旦稳定,多数
useMemo / useCallback / React.memo不用手写。 - 现阶段保持关注,业务可以小范围试用。
24.11 性能检查清单
- React DevTools Profiler 录过一遍
- state 是否能下移到更小的子组件
- 大列表是否虚拟化
- 路由级 / 重组件级有没有 code split
- Context 是否拆开 / 用 selector
- 网络层是否做了缓存(react-query)
- 首屏图片有没有懒加载、字体有没有 preload
- 包体积有没有
vite-plugin-visualizer/webpack-bundle-analyzer看过
25. 测试
25.1 选型
| 层级 | 工具 | 类比 |
|---|---|---|
| 单测 / 组件测试 | Vitest + @testing-library/react | Vue Test Utils + Vitest |
| 浏览器 E2E | Playwright(推荐)/ Cypress | 同 Vue |
| 视觉回归 | Chromatic / Loki | — |
| 组件预览 | Storybook | 同 Vue |
Vite 项目用 Vitest 几乎零配置。Jest 不是不能用,但 ESM / TS / Vite 体系下 Vitest 更顺。
25.2 安装
npm i -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
vite.config.ts:
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
globals: true,
},
})
src/test/setup.ts:
import '@testing-library/jest-dom/vitest'
25.3 组件测试(核心思想:从用户视角)
// Counter.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Counter from './Counter'
test('点击按钮 count 加 1', async () => {
render(<Counter />)
const btn = screen.getByRole('button', { name: /\+/ })
expect(btn).toHaveTextContent('0')
await userEvent.click(btn)
expect(btn).toHaveTextContent('1')
})
要点:
- 按角色 / 文本找元素(
getByRole / getByText / getByLabelText),不要按 class / id。 - 用
userEvent模拟真实交互,不要直接fireEvent。 - 别测实现细节(state 名字、内部方法),测用户能感知的结果。
25.4 mock 请求
// 用 msw(Mock Service Worker)
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
const server = setupServer(
http.get('/api/users', () => HttpResponse.json([{ id: 1, name: 'A' }]))
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
业务里所有 fetch / axios 都自动被拦截。
25.5 自定义 hook 测试
import { renderHook, act } from '@testing-library/react'
import { useCounter } from './useCounter'
test('useCounter', () => {
const { result } = renderHook(() => useCounter())
expect(result.current.count).toBe(0)
act(() => result.current.inc())
expect(result.current.count).toBe(1)
})
25.6 E2E(Playwright)
npm init playwright@latest
import { test, expect } from '@playwright/test'
test('登录流程', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('用户名').fill('admin')
await page.getByLabel('密码').fill('123456')
await page.getByRole('button', { name: '登录' }).click()
await expect(page).toHaveURL('/dashboard')
})
25.7 测什么、不测什么
| 该测 | 不该测 |
|---|---|
| 业务流程(登录、下单) | 第三方库自身 |
| 自定义 hook 的逻辑 | UI 库的内部行为 |
| 关键工具函数 | 样式细节 |
| 容易回归的边界条件 | 实现细节(私有方法、内部 state 名) |
经验值:业务后台先把 E2E 写好 5–10 条主流程,性价比远高于组件单测铺满。
26. TypeScript 高阶用法(React 场景)
26.1 组件 Props 的几种写法
// 1. 最常用:函数参数解构 + type
type ButtonProps = {
type?: 'primary' | 'default'
loading?: boolean
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
children: React.ReactNode
}
function Button({ type = 'default', loading, onClick, children }: ButtonProps) {
return <button onClick={onClick}>{children}</button>
}
// 2. 不推荐:React.FC(默认隐式有 children、默认值类型不友好)
const Bad: React.FC<ButtonProps> = (props) => <button />
26.2 常见类型速查
| 场景 | 类型 |
|---|---|
| 可渲染内容(任意 JSX / 字符串 / null) | React.ReactNode |
| 单个 React 元素 | React.ReactElement |
| 事件 | React.MouseEvent<HTMLButtonElement> / ChangeEvent<HTMLInputElement> |
| ref 指向 input | React.RefObject<HTMLInputElement> |
| 转发 ref 的组件 | React.ForwardedRef<T> |
| 样式对象 | React.CSSProperties |
| 子组件作为函数 | (args: T) => React.ReactNode |
26.3 useState 的类型推导
const [n, setN] = useState(0) // number
const [user, setUser] = useState<User | null>(null) // 显式联合
const [list, setList] = useState<Item[]>([]) // 空数组要显式
26.4 useRef 三种用法
const inputRef = useRef<HTMLInputElement>(null) // 给 DOM
const timerRef = useRef<number | null>(null) // 存可变值
const initRef = useRef(false) // 标志位
26.5 useReducer 强类型
type State = { count: number }
type Action = { type: 'inc' } | { type: 'set'; payload: number }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'inc': return { count: state.count + 1 }
case 'set': return { count: action.payload }
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 })
dispatch({ type: 'set', payload: 10 }) // ✅ 类型严格
26.6 forwardRef + 泛型组件
import { forwardRef } from 'react'
type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
label?: string
}
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{ label, ...rest },
ref,
) {
return (
<label>
{label}
<input ref={ref} {...rest} />
</label>
)
})
26.7 泛型组件(比如通用 List)
type ListProps<T> = {
data: T[]
renderItem: (item: T) => React.ReactNode
keyOf: (item: T) => React.Key
}
function List<T>({ data, renderItem, keyOf }: ListProps<T>) {
return <ul>{data.map(item => <li key={keyOf(item)}>{renderItem(item)}</li>)}</ul>
}
<List
data={users}
keyOf={u => u.id}
renderItem={u => <span>{u.name}</span>}
/>
26.8 Discriminated Union 收窄 props
type Props =
| { mode: 'view'; data: User }
| { mode: 'edit'; data: User; onChange: (u: User) => void }
function UserPanel(props: Props) {
if (props.mode === 'edit') {
props.onChange // ✅ 这里 TS 知道有 onChange
}
return <div>{props.data.name}</div>
}
业务上区分「只读 vs 编辑」「单选 vs 多选」非常好用。
26.9 as const + satisfies(避免类型变宽 / 又不丢类型校验)
const statusMap = {
1: '待派工',
2: '已派工',
3: '已完成',
} as const satisfies Record<number, string>
type Status = keyof typeof statusMap // 1 | 2 | 3
适合后台项目里的状态枚举、map。
26.10 工具类型常用
type Partial<T> // 全部可选
type Required<T> // 全部必填
type Pick<T, K> // 选字段
type Omit<T, K> // 去字段
type Record<K, V> // 字典
type ReturnType<typeof fn> // 取函数返回值
type Awaited<T> // 解包 Promise
type NonNullable<T> // 去 null/undefined
业务里最常用的是 Pick / Omit / Partial,比如:
type UserVO = { id: number; name: string; age: number; password: string }
type UserCreateDTO = Omit<UserVO, 'id'>
type UserUpdateDTO = Partial<Omit<UserVO, 'id'>> & { id: number }
type UserListItem = Pick<UserVO, 'id' | 'name'>
26.11 严格 tsconfig 推荐
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"verbatimModuleSyntax": true
}
}
noUncheckedIndexedAccess 会让 arr[0] 推成 T | undefined,避免运行期 undefined.xxx 崩溃,强推。
26.12 业务里实用 Pattern
API 返回 + Hook 强类型:
type ApiRes<T> = { code: number; data: T; message: string }
async function request<T>(url: string): Promise<T> {
const res = await fetch(url)
const json: ApiRes<T> = await res.json()
if (json.code !== 0) throw new Error(json.message)
return json.data
}
// 使用
const users = await request<User[]>('/api/users')
全局 Store 类型推导(Zustand):
import { create } from 'zustand'
interface UserStore {
user: User | null
setUser: (u: User | null) => void
}
export const useUserStore = create<UserStore>((set) => ({
user: null,
setUser: (u) => set({ user: u }),
}))
27. 进阶部分的「下一步」建议
读完进阶部分后,建议你按需深入:
| 想做的事 | 看什么 |
|---|---|
| 写 C 端 / SEO 页面 | Next.js 官方 tutorial + RSC 文档 |
| 优化业务后台卡顿 | React DevTools Profiler + react-virtual |
| 团队上 TS 规范 | typescript-eslint strict 规则 |
| 提升交付质量 | Playwright + msw 起一套 E2E |
| 跟进新特性 | 关注 React Compiler、useOptimistic、useFormStatus、useActionState |
更多推荐



所有评论(0)