这是一篇边学边记的 React 笔记,目标不是抄文档,而是把「什么时候该用什么」讲清楚。示例代码基于 React 18+,大部分写法在 React 19 同样适用,有差异的地方单独标注。

一、React 到底是什么

一句话:React 是一个用于构建用户界面的 JavaScript 库,它只负责「数据 → 界面」这一层,路由、请求、状态管理都交给生态。

三个核心观念,理解了它们就理解了 React 的一半:

观念 含义 带来的好处
声明式 你描述「界面应该长什么样」,而不是「一步步怎么改 DOM」 不用手写 DOM 操作,心智负担低
组件化 UI 拆成独立、可复用的函数(组件) 复用、隔离、好测试
单向数据流 数据从父组件流向子组件,子组件不能直接改父组件的数据 数据变化可追踪,出 bug 好定位

对比一下就明白了。同样是「点击按钮后把数字加一」:

1
2
3
4
5
6
7
8
9
10
// 命令式(原生 JS)—— 需要自己管 DOM
let count = 0
btn.onclick = () => {
count += 1
el.textContent = count // 手动更新
}

// 声明式(React)—— 只描述结果
const [count, setCount] = useState(0)
<button onClick={() => setCount(count + 1)}>{count}</button>

二、环境搭建

现在官方推荐用 Vite,create-react-app 已经不推荐在新项目里使用了。

1
2
3
4
5
6
7
# 创建项目(选择 React + JavaScript/TypeScript)
npm create vite@latest my-app -- --template react

cd my-app
npm install
npm run dev # 启动开发服务器,默认 http://localhost:5173
npm run build # 打包产物输出到 dist/

生成的关键文件:

1
2
3
4
5
6
7
my-app/
├── index.html # 唯一的 HTML 入口
├── src/
│ ├── main.jsx # 挂载点,把 <App /> 渲染到 #root
│ ├── App.jsx # 根组件
│ └── components/ # 自己建的组件目录(约定俗成)
└── vite.config.js

main.jsx 里这几行是整个应用的起点:

1
2
3
4
5
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

ReactDOM.createRoot(document.getElementById('root')).render(<App />)

三、JSX 语法要点

JSX 不是模板字符串,也不是 HTML,它会被编译成 React.createElement() 调用。和 HTML 的差异是新手最容易踩坑的地方:

场景 HTML 写法 JSX 写法 原因
类名 class="box" className="box" class 是 JS 关键字
内联样式 style="color:red" style={{ color: 'red' }} 外层 {} 是表达式,内层 {} 是对象
label for="name" htmlFor="name" 同上,for 是关键字
自闭合 <br> <br /> JSX 要求标签闭合
注释 <!-- x --> {/* x */} JSX 里只能写 JS 注释

三条规则记住就不会错:

  1. 只能返回一个根元素 —— 多个并列元素用 <> </>(Fragment)包起来
  2. {} 里可以放任何 JS 表达式,但不能放语句(如 if、for)
  3. 属性名用驼峰:onclick → onClick,tabindex → tabIndex
1
2
3
4
5
6
7
8
9
10
11
function UserCard({ user }) {
return (
<>
<h2 style={{ fontSize: 18 }}>{user.name}</h2>
{/* 三元表达式可以写在 {} 里 */}
<span className={user.vip ? 'tag tag--vip' : 'tag'}>
{user.vip ? 'VIP' : '普通用户'}
</span>
</>
)
}

四、组件与 Props

组件就是首字母大写的函数,返回一段 JSX。Props 是父组件传进来的参数,是个只读对象。

1
2
3
4
5
6
7
// 子组件:通过 props 接收
function Button({ text, type = 'default', onClick }) {
return <button className={`btn btn--${type}`} onClick={onClick}>{text}</button>
}

// 父组件:像写 HTML 属性一样传值
<Button text="提交" type="primary" onClick={handleSubmit} />

Props 是只读的,永远不要在子组件里写 props.name = 'xxx'。组件内部要变的数据用 state,要通知父组件就调用父组件传下来的回调函数。

children 是个特殊的 prop,代表标签之间的内容:

1
2
3
4
5
6
7
8
9
10
function Card({ title, children }) {
return (
<div className="card">
<h3>{title}</h3>
<div className="card__body">{children}</div>
</div>
)
}

<Card title="提示">这里是正文内容</Card>

五、State 与事件处理

useState 是组件内部的「可变数据」。调用 setXxx 会让组件重新渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useState } from 'react'

function Counter() {
const [count, setCount] = useState(0)

// ✅ 正确:传函数,基于最新值计算
const add = () => setCount(c => c + 1)

// ❌ 错误示范:连续调用两次也只加 1,因为 count 是本次渲染的快照
const wrongAdd = () => {
setCount(count + 1)
setCount(count + 1)
}

return <button onClick={add}>{count}</button>
}

state 更新是异步且批量的:调用 setCount 后立刻 console.log(count) 拿到的是旧值。要拿到更新后的值,用 useEffect 监听,或者直接用新值继续计算。

事件绑定的三种写法,注意括号:

1
2
3
<button onClick={handleClick}>传函数引用</button>        {/* ✅ 推荐 */}
<button onClick={() => handleClick(id)}>需要传参</button> {/* ✅ 用箭头函数包一层 */}
<button onClick={handleClick()}>错误写法</button> {/* ❌ 渲染时就执行了 */}

六、条件渲染与列表渲染

1
2
3
4
5
6
7
8
9
10
// 1. 三元表达式:二选一
{isLogin ? <UserPanel /> : <LoginBtn />}

// 2. && :满足条件才显示(别用数字 0 做条件,会渲染出一个 "0")
{list.length > 0 && <List data={list} />}

// 3. 早期 return:分支复杂时最清晰
if (loading) return <Loading />
if (error) return <Error msg={error.message} />
return <Content data={data} />

列表渲染用 map,必须写 key:

1
2
3
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}

key 不要用数组下标(index)。删除中间某一项时下标会错位,导致 React 复用错误的 DOM 和组件状态 —— 表单输入框内容错位就是这么来的。用数据里稳定的 id。

七、表单:受控组件

React 里推荐「受控组件」:表单的值由 state 保管,value 与 onChange 成对出现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Form() {
const [form, setForm] = useState({ name: '', email: '' })

const handleChange = e => {
const { name, value } = e.target
setForm(prev => ({ ...prev, [name]: value })) // 展开旧值再覆盖
}

const handleSubmit = e => {
e.preventDefault() // 阻止表单默认提交导致页面刷新
console.log(form)
}

return (
<form onSubmit={handleSubmit}>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<button type="submit">提交</button>
</form>
)
}

八、Hooks 逐个拆解

Hooks 就是「在函数组件里使用状态等 React 特性」的函数。规则只有两条:只能在组件顶层调用(不能放进 if / for / 嵌套函数)、只能在组件或自定义 Hook 里调用。

useState

1
const [state, setState] = useState(初始值)

初始值可以传函数(懒初始化,只在首次渲染执行,适合开销大的计算):

1
const [list, setList] = useState(() => JSON.parse(localStorage.getItem('list')) || [])

useEffect —— 处理「副作用」

副作用 = 渲染之外的事情:请求数据、订阅、操作 DOM、定时器。

1
2
3
4
useEffect(() => {
const timer = setInterval(() => setTime(Date.now()), 1000)
return () => clearInterval(timer) // 👈 清理函数,组件卸载时执行
}, []) // 👈 依赖数组

依赖数组的三种情况,务必分清:

依赖数组 执行时机 典型场景
不写 每次渲染后都执行 几乎不用,容易死循环
[] 只在挂载后执行一次 初始化请求、绑定全局事件
[a, b] 挂载时 + a/b 变化时 按条件重新请求数据

依赖数组里必须写全 effect 中用到的所有会变化的外部变量(state、props、组件内定义的函数),否则会拿到过期的旧值 —— 这是 useEffect 最经典的坑。装上 eslint-plugin-react-hooks 可以自动提示。

useRef —— 拿 DOM 或存「不触发渲染的值」

1
2
3
4
const inputRef = useRef(null)
const focus = () => inputRef.current.focus()

return <input ref={inputRef} />

改 ref.current 不会触发重新渲染,所以它适合存定时器 ID、上一次的值这类不需要显示在界面上的数据。

useMemo / useCallback —— 缓存

1
2
const expensive = useMemo(() => heavyCalc(list), [list])       // 缓存计算结果
const handleClick = useCallback(() => doSomething(id), [id]) // 缓存函数引用

别滥用。二者本身也有开销,只在这两种情况下用:① 计算确实很重;② 函数要传给用 React.memo 包过的子组件(否则每次渲染函数引用都变,memo 白做)。

useContext —— 跨层传递

解决「props 一层层往下传」的地狱:

1
2
3
4
5
6
7
8
9
10
// 1. 创建
const ThemeContext = createContext('light')

// 2. 顶层提供
<ThemeContext.Provider value="dark">
<App />
</ThemeContext.Provider>

// 3. 任意深层消费
const theme = useContext(ThemeContext)

useReducer —— 复杂状态逻辑

state 结构复杂、更新逻辑多时,比 useState 清晰得多(Redux 的迷你版):

1
2
3
4
5
6
7
8
9
10
function reducer(state, action) {
switch (action.type) {
case 'add': return { ...state, count: state.count + 1 }
case 'reset': return { count: 0 }
default: return state
}
}

const [state, dispatch] = useReducer(reducer, { count: 0 })
dispatch({ type: 'add' })

自定义 Hook —— 逻辑复用

把重复逻辑抽出来,名字以 use 开头:

1
2
3
4
5
6
7
8
function useLocalStorage(key, initial) {
const [value, setValue] = useState(() => localStorage.getItem(key) ?? initial)
useEffect(() => { localStorage.setItem(key, value) }, [key, value])
return [value, setValue]
}

// 用起来
const [theme, setTheme] = useLocalStorage('theme', 'light')

九、组件通信方式汇总

场景 方案
父 → 子 props
子 → 父 父传回调函数,子调用
兄弟组件 状态提升到共同的父组件
跨多层 Context / 状态管理库
任意组件 Zustand / Redux 等状态管理库

十、数据请求

推荐用 axios,比原生 fetch 好用(自动 JSON 解析、拦截器、超时控制)。

1
2
3
4
5
6
7
8
9
10
useEffect(() => {
let cancelled = false // 防止组件已卸载还 setState(竞态问题)

axios.get('/api/users', { params: { page } })
.then(res => { if (!cancelled) setUsers(res.data) })
.catch(err => { if (!cancelled) setError(err.message) })
.finally(() => { if (!cancelled) setLoading(false) })

return () => { cancelled = true }
}, [page])

正经项目建议直接用 TanStack Query(原 React Query),它把 loading / error / 缓存 / 重试 / 竞态全包了,能省掉上百行样板代码。

十一、路由(React Router v6)

1
npm install react-router-dom
1
2
3
4
5
<Routes>
<Route path="/" element={<Home />} />
<Route path="/posts/:id" element={<Post />} />
<Route path="*" element={<NotFound />} />
</Routes>
1
2
3
4
5
6
7
8
9
10
// 编程式跳转
const navigate = useNavigate()
navigate(`/posts/${id}`)

// 取路径参数 /posts/123 → id = 123
const { id } = useParams()

// 取查询串 ?page=2
const [search] = useSearchParams()
const page = search.get('page')

十二、状态管理怎么选

组件不多、共享状态少 → Context + useReducer 就够用,别一上来就引入 Redux。

推荐 Zustand:API 极简(一个 create 搞定)、不需要 Provider 包裹、体积约 1KB。

1
2
3
4
5
6
7
import { create } from 'zustand'
const useStore = create(set => ({
count: 0,
add: () => set(s => ({ count: s.count + 1 })),
}))
// 组件里直接用
const { count, add } = useStore()

Redux Toolkit:严格的单向数据流、时间旅行调试、完善的中间件体系,团队协作规范性强,代价是样板代码偏多。

十三、性能优化清单

  • 列表项组件用 React.memo 包裹,父组件重渲染时跳过未变化的子项
  • 传给 memo 子组件的回调函数用 useCallback 包住,避免引用变化让 memo 失效
  • 长列表用虚拟滚动(react-window / react-virtuoso),只渲染可视区域
  • 路由级代码分割:const Page = lazy(() => import('./Page')) 配合 <Suspense>
  • 图片懒加载 + CDN + 合适尺寸(可参考本站的封面缩放做法)

十四、常见坑速查

现象 原因 解法
setState 后读到旧值 更新是异步批量的 用 useEffect 监听,或直接用新值
连续 setState 只生效一次 用了快照值而非函数式更新 改成 setCount(c => c + 1)
useEffect 死循环 依赖里放了每次渲染都变的对象/数组 用 useMemo 稳定引用,或调整依赖
列表删除后输入框错位 用 index 当 key 换成数据里的稳定 id
样式没生效 写了 class 而不是 className 改 className
事件里 e.target 变成 null React 的事件对象会被回收 先解构 const { value } = e.target

十五、React 与 Vue 对照

能力 React Vue 3
模板 JSX(JS 里写 HTML) template(HTML 里写 JS)
状态 useState ref / reactive
副作用 useEffect + 依赖数组 watch / watchEffect(自动收集依赖)
计算属性 useMemo computed
组件通信 props + 回调 props + emit
跨层传递 Context provide / inject

两者思想高度相似(虚拟 DOM + 响应式 + 组件化),精通一个后学另一个的成本很低。


学习路径建议:JSX → 组件/Props → useState 与事件 → 条件与列表渲染 → useEffect → 表单 → 自定义 Hook → 路由 → 状态管理。每一步都配一个小 demo,比通读文档有效得多。