React 快速了解基础
在最新的前端开发技术中,React 是一个非常流行且被广泛采用的 JavaScript 库 之一。React 以声明式的方式处理用户界面,使开发者能够使用组件来构建可重用和可维护的界面。这篇文章将会带你从基础入门开始,愉快的学习 React!
参考 https://react.dev/learn
编辑器:VS Code(装插件:ESLint、Prettier、React Developer Tools 浏览器扩展)
1. 项目结构速览(脚手架生成)
许多工具都能够创建一个 react 模板项目,如 React Router (v7)。
npx create-react-router@latest
脚手架会生成一个以 数据路由(Data Routers) 为中心的结构,大致包含:
| 文件夹/文件 | 说明 |
|---|---|
app/ | 主要代码目录(类似 Remix) |
┣ routes/ | 路由文件夹(文件即路由) |
┃ ┣ home.tsx | /home 页面组件 |
┃ ┗ routes.ts | 导出路由定义(自动生成用) |
┣ welcome/ | 可选嵌套路由或组件目录 |
┣ root.tsx | 根路由,包含全局布局、导航、错误边界等 |
┗ app.css | 全局样式 |
react-router.config.ts | React Router 配置文件(例如 SSR 设置) |
vite.config.ts | Vite 构建配置 |
tsconfig.json | TypeScript 配置 |
Dockerfile | 容器部署配置(可忽略) |
v7 的理念: 组件路由依然可用,但推荐路由模块(文件即路由)+ 数据 API(loader/action) ;同时 v7 保持与 v6 的非破坏升级,并向 React 19 的服务端能力过渡。React Router
2. 启动项目
# 进入项目
cd my-react-router-app
# 安装依赖
npm install
# 启动开发服务器
npm run dev
默认情况下启动在 5173 端口
3. React 基础:组件 + JSX + 状态
我们先从最核心的 React 概念讲起:
- 什么是组件?
export default function Profile() {
return (
<img
src="https://i.imgur.com/MK3eW3Am.jpg"
alt="Katherine Johnson"
/>
)
}
在这个例子中:
Profile是组件名。- 它返回一个
<img />标签。 - 通过
export default导出,便于在其他文件中import Profile from './Profile'使用。
- 如何使用组件?
你定义好了组件,就可以在其他组件中像使用 HTML 标签那样使用它:
function Profile() {
return (
<img
src="https://i.imgur.com/MK3eW3Am.jpg"
alt="Katherine Johnson"
/>
);
}
export default function Gallery() {
return (
<section>
<h1>Amazing scientists</h1>
<Profile />
<Profile />
<Profile />
</section>
);
}
注意:
<section>是 HTML 原生标签,因为首字母是小写。<Profile />是我们定义的组件,因为首字母大写。组件可以被重复使用、嵌套组合,从而构建复杂界面。
组件传递参数方式:
示例(简化版):
// 父组件
<Avatar
person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }}
size={100}
/>
在 Avatar 组件上,我们传递了两个 props:person(一个对象)和 size(一个数字)React
- 读取 props
在子组件中,你可以通过函数参数接收到 props 对象,例如:
function Avatar({ person, size }) {
// 此处 person 和 size 可用
return (
<img
src={getImageUrl(person)}
alt={person.name}
width={size}
height={size}
/>
);
}
说明:
function Avatar(props)是传统形式;而function Avatar({ person, size })是通过 解构赋值 (destructuring)来直接获取所需属性。 React- 在子组件内部,就像使用局部变量一样使用这些 props。
React 组件可以接收 children prop,表示标签里嵌套的内容(children 是一个 component):
function Card({ children }) {
return <div className="card">{children}</div>;
}
// 使用
<Card>
<Avatar size={100} person={…} />
</Card>
在这个用法中,<Avatar …/> 被当作 Card 的 children,使 Card 能包裹任意内容。 React 这是构建可组合 UI 组件(Panel、Wrapper、布局容器等)非常常见的模式。
默认导出(default export) vs 命名导出(named export)
::: important
引用和定义组件请使用大写开头。
注意:一个文件最多只能有一个默认导出,但可以有多个命名导出。
不要在一个 component 里面定义另一个 component。
| Syntax | Export statement | Import statement |
|---|---|---|
| Default | export default function Button() {} | import Button from './Button.js'; |
| Named | export function Button() {} | import { Button } from './Button.js'; |
:::
- JSX
JSX 是 JavaScript 的一个语法扩展,允许你在 JS 文件里写看起来像 HTML 的标记(markup)来描述 UI。虽然不是必须,但大多数 React 开发者更喜欢使用 JSX,因为它让 “渲染逻辑” 与 “标记内容” 紧密结合。
在传统 Web 开发中:内容 (HTML)、样式 (CSS)、逻辑 (JavaScript) 往往分离;但随着交互变多,逻辑主导内容越来越常见。JSX 支持将逻辑 + 标记放在一起。
示例: 一个常见的 HTML 片段:
<h1>Hedy Lamarr's Todos</h1>

<ul>
<li>Invent new traffic lights</li>
<li>Rehearse a movie scene</li>
<li>Improve the spectrum technology</li>
</ul>
如果直接复制到 JSX 中,会报错。原因是 JSX 有一些更严格或不同的规则。 React
主要差别包括:
- 必须有一个单一根元素(或使用 Fragment)
- 所有标签必须闭合,包括自闭合标签(如
<img />) - 属性名大多使用 camelCase(而不是 HTML 的 kebab-case 或 “class”)
- 某些 HTML 属性名在 JSX 中被替换,比如
class→className,stroke-width→strokeWidth等。
以下为正确 jsx 示例
export default function TodoList() {
return (
<>
<h1>Hedy Lamarr's Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
className="photo"
/>
<ul>
<li>Invent new traffic lights</li>
<li>Rehearse a movie scene</li>
<li>Improve the spectrum technology</li>
</ul>
</>
);
}
JSX 中的条件渲染
- 在 React 中,用 JavaScript 控制分支逻辑(if、? :、&&)。 React
{ cond ? <A /> : <B /> }表示“如果 cond 渲染 A,否则 B”。 React{ cond && <A /> }表示“如果 cond 渲染 A,否则什么都不渲染”。 React- 虽然快捷语法常见,但也可以回到最基础的 if 语句,以确保清晰。 React
function Item({ name, isPacked }) {
if (isPacked) {
return <li className="item">{name} ✅</li>;
// 什么都不想渲染,可以 return null
}
return <li className="item">{name}</li>;
}
JSX 中渲染列表
const people = [
{ id: 0, name: 'Creola Katherine Johnson', profession: 'mathematician', … },
{ id: 1, name: 'Mario José Molina-Pasquel Henríquez', profession: 'chemist', … },
…
];
const chemists = people.filter(person =>
person.profession === 'chemist'
);
const listItems = chemists.map(person =>
<li key={person.id}>
<img src={getImageUrl(person)} alt={person.name} />
<p>
<b>{person.name}:</b> {person.profession} known for {person.accomplishment}
</p>
</li>
);
return <ul>{listItems}</ul>;
- 将数据从组件中抽离出来(用数组/对象)。 React
- 使用
map()对数据进行转换 → 渲染成多个组件/元素。 React - 使用
filter()可以先筛选一部分数据,再渲染。 React - 每个列表项要设置
key属性,保证 React 能正确追踪。 React
4. 事件处理
React 允许你在 JSX 中给元素添加事件处理器(event handlers),用来响应点击 (click)、悬浮 (hover)、输入焦点 (focus)、表单提交 (submit) 等用户交互。
在 JSX 元素上通过像 onClick={…} 的 prop 来传入一个函数。 React+1
常见步骤:
- 在组件内部声明一个函数(如
handleClick) - 在函数体内实现逻辑(例如
alert('You clicked me!')) - 在 JSX 中
<button onClick={handleClick}>…</button>。 React
函数可以定义在组件里,也可以用内联(inline)匿名函数或箭头函数。 React
重要坑 :必须 传递 函数,而不是 调用 函数。当你写 onClick={handleClick()} 则会在渲染时立即执行,而不是在点击时执行。
将事件处理器作为 Props 传递(Passing event handlers as props)
要点 :
如果你有一个通用组件(例如
Button),它并不知道每次点击应该做什么。你可以把行为(函数)作为 prop 传给它:function Button({ onClick, children }) { return <button onClick={onClick}>{children}</button>; }父组件决定要做的事情,比如:
function PlayButton({ movieName }) { function handlePlayClick() { alert(`Playing ${movieName}!`); } return <Button onClick={handlePlayClick}>Play "{movieName}"</Button>; }关于事件 handler prop 命名的约定:对于自定义组件,通常将 prop 名以
on…开头,如onClick、onSmash等。这样代码易读、意义明确。
5. 状态
在 React 中,用 Hook useState 来创建状态变量。文档示例:
import { useState } from 'react';
const [index, setIndex] = useState(0);
注意 Hooks 的使用规则:
- 只能在函数组件的最顶层调用,不能在循环、条件或嵌套函数里调用。React
- 这保证每次渲染时
useState被调用的次数与顺序不变,这是 React 区分不同状态变量的关键。
状态的「私有性」与“隔离”
- 状态是 组件实例 专属的。若你在 UI 上渲染同一个组件两次, 每一个实例 都会有自己的状态拷贝,互不影响。
- 父组件无法直接 “操控” 子组件内部声明的状态(除非你通过 props 或回调把状态提升上来)。状态默认只是组件自己的“私有记忆”。
在一次渲染周期(render)中,state 的值 对该渲染函数及其事件处理器而言是固定的 。如果在同一个渲染中多次 setState(state + 1),那么每次 state 的 “快照值” 均不会更新,因此可能不会按你预期累加多次。示例:点击 “+3” 按钮只加 1。
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
在写 useState + 事件处理的时候, 假设 state 在当前渲染中是固定不变的 。
避免在一系列 setState(state + 1) 中预期“连续更新”而不使用函数式更新。
如果你的处理逻辑需要 “最新” state 值(如 +3、+5、基于当前值累加等),建议改写为:
setNumber(prev => prev + 1);
setNumber(prev => prev + 1);
setNumber(prev => prev + 1);
或者直接:setNumber(prev => prev + 3);
文中强调:你应当把 state 中的对象当作“只读”的(read-only)。不要直接修改。
但如果你想要修改 state 中的对象,举例:
const [position, setPosition] = useState({ x:0, y:0 });
position.x = e.clientX; // 直接修改对象 —— 问题在这里
这种做法会导致 React 无法检测到变化 ,所以组件不会重新渲染。
如果你的 state 存的是一个对象,而你只想更新对象中的一个字段,可以使用对象的拷贝语法(spread)来创建新的对象。
setPerson({
...person, // 复制以前的所有字段
firstName: e.target.value // 覆盖某个字段
});
这样可以避免手动写出所有字段,比较方便。
- 状态中的 Array
React 的状态更新机制依赖 引用变化 来决定是否需要重新渲染。若直接改变已有数组(如 arr.push() 或 arr[0] = …),原数组引用不变,可能导致 React 无法识别变化 ,或者造成难以发现的副作用。
1. 添加元素(Add)
不推荐: 使用 push() 等会改变原数组的方法。 React+1推荐: 使用扩展运算符(spread)和/或 concat() 创建新数组。示例:
// TypeScript + React
const [items, setItems] = useState<MyItemType[]>([]);
// 在末尾添加:
setItems([
...items,
newItem
]);
// 在开头添加:
setItems([
newItem,
...items
]);
上述方法会生成一个新的数组引用,从而触发 React 重新渲染。 React
2. 删除元素(Remove)
推荐使用 filter() 方法:
setItems(
items.filter(item => item.id !== targetId)
);
这样得到的是一个 新数组 ,原数组不被修改。 React
3. 转换/更新元素(Transform / Replace)
当你想更新数组中某些元素(例如改变某项属性):
推荐使用 map():
setItems(
items.map(item =>
item.id === targetId
? { ...item, someProp: newValue } // 创建新对象
: item // 保持原对象
)
);
这种方式保证 “数组引用” 和 “被替换对象” 都是新的。否则如果你只是修改旧对象,就会和旧状态共享对象引用,可能引发难以察觉的 bug。 React
4. 插入元素到中间位置(Insert)
如果要在数组中间某个位置插入一个新项:
const index = 1;
setItems([
...items.slice(0, index),
newItem,
...items.slice(index)
]);
这里 slice() 返回的是一个新的子数组,因此你在拼出新的数组时也是在构造一个新的结构。 React
5. 其他需要注意的变动(排序、反转、深层嵌套修改)
- 方法如
sort()、reverse()会 原地修改数组 ,如果直接在 state 的数组上调用,会破坏不可变原则。推荐先做const newArr = [...oldArr],然后newArr.sort()或newArr.reverse(),最后setItems(newArr)。 React - 注意:即便你复制了数组(如
[...oldArr]),如果数组元素是对象,那么这些对象还是旧的引用。如果你修改了其中某个对象的属性,也可能是直接修改原对象,仍然会破坏 state 的不可变性。文档举了一个例子:拷贝数组但未深拷贝对象,结果两个状态 hook 共享对象引用,操作一个会影响另一个。 React
6. Reducer
当有一个组件的状态逻辑非常复杂(比如需要进行添加,删除,修改等操作),可以考虑使用 reducer,而不是 state。在 Redux、MobX 等状态管理库流行之前,这种模式就已被广泛采用。React 自身也鼓励在适合场景下使用 useReducer。
编写 reducer 函数
reducer函数签名通常为(state, action) => nextState。 React在例子中,
tasksReducer(tasks, action)根据action.type返回新的数组状态:function tasksReducer(tasks, action) { switch(action.type) { case 'added': return [...tasks, { id: action.id, text: action.text, done: false }]; case 'changed': return tasks.map(t => t.id === action.task.id ? action.task : t); case 'deleted': return tasks.filter(t => t.id !== action.id); default: throw Error('Unknown action: ' + action.type); } }写好之后,可以把它提取到组件外部或独立文件,使组件更简洁。 React
在组件中使用 useReducer
在组件里用:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);替代之前的
const [tasks, setTasks] = useState(initialTasks)。 React然后在事件处理器里调用
dispatch(...)。比如:function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text }); }
7. Context
Context 是一种“跨层级”传递数据的方式,避免中间组件的重复传递。
使用 Context 的典型场景 :
- 主题(theme):全局颜色、暗黑模式等。React
- 当前用户 (current account / auth):很多组件可能需要知道登录状态或用户信息。React
- 路由状态:其实路由库本身就用 context 来实现的。React
- 大型状态管理:结合
useReducer + Context传递复杂状态至深层组件。
1. 创建 Context
import { createContext } from 'react';
export const LevelContext = createContext(1);
这里 1 是默认值,当组件在树中“没有”被对应的 Provider 包裹时,会用这个默认值。React
记住:
createContext(defaultValue)的 defaultValue 是有意义的,在没有 Provider 时也能有“兜底”的机制。
2. 在需要读取数据的组件中使用 useContext
import { useContext } from 'react';
import { LevelContext } from './LevelContext';
export default function Heading({ children }) {
const level = useContext(LevelContext);
// …
}
这样 Heading 就不再依赖自己被传 level prop,而是“自己去找”最近的 LevelContext 提供者(根据当前 component 所在位置,往父组件方向寻找 LevelContext.Prodiver)。React
3. 在提供数据的组件中使用 Provider
import { LevelContext } from './LevelContext';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
这样 Section 把 level 值“提供”给其下层所有用到 LevelContext 的组件
8. Ref
当一个组件希望 “记住(remember)” 某些信息,但 不希望这些信息的变化触发重新渲染(re-render) 时,就可以使用 useRef()
1. 导入 useRef
import { useRef } from 'react';
2. 在组件内部调用,指定初始值
const myRef = useRef(0);
这里 myRef.current 初始为 0。 React
3. 访问或修改 ref.current
例如:
myRef.current = myRef.current + 1;
alert('当前值:' + myRef.current);
此操作不会导致组件重新渲染(除非你自己用 state 做更新)––因此,用来存储 “渲染之外的可变值” 十分合适。 React
| 特性 | refs (useRef) | state (useState) |
|---|---|---|
| 返回值 | { current: initialValue } 对象 (React) | [value, setValue] 数组 (React) |
| 变化是否触发渲染 | 不会 (React) | 会 (React) |
| 值是否可变 | 可变(你可以直接写 ref.current = ...) (React) | “不可变”意义上,必须通过 setValue(...) 更新 |
| 在渲染期间读取 | 不推荐在渲染过程中读取/写入 ref.current,因为 React 无法追踪它,可能导致不可预测行为。 (React) | 可以直接在渲染函数中读取 state,React 会正确追踪 |
| 用途 | 存储:定时器 ID、浏览器 APIs、DOM 元素引用、过程状态等,不影响 UI 渲染。 (React) | 存储会用于 UI 显示或决定渲染逻辑的数据(用户输入、是否展示、计数器等) |
Ref 使用示例
import { useRef } from 'react';
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
为何需要 Refs(引用)? React 默认负责更新 DOM,以匹配组件输出。通常你不需要手动操作 DOM。但在某些情形(如:让输入框自动聚焦、滚动到某个元素、测量元素尺寸/位置)下,就需要获取 React 管理的 DOM 节点。 React 在这些情况下,就可以用 “ref” 来访问 DOM 节点并调用浏览器原生 API。 React
如何获取 DOM 节点引用?
- 使用
useRef()Hook 创建一个 ref 对象(例如const myRef = useRef(null);) React - 在 JSX 元素上加上
ref={myRef},React 在 commit 阶段会把该 DOM 节点赋值给myRef.current。 React - 注意:在 render 阶段
myRef.current可能仍为null,因为 DOM 节点尚未创建或尚未更新。React
何时使用 Refs? 常见场景包括:
- 让输入框聚焦(focus) React
- 滚动到某个元素(scrollIntoView) React
- 测量元素大小、位置(getBoundingClientRect)
- 或者调用浏览器 API(如 video 的 play/pause) React 也就是说,Refs 是一种 “逃生舱”(escape hatch)——在 React 控制之外需要直接操作 DOM 时才用。
9. Effect
在 React 里,组件主要做两件事:
- 渲染逻辑 :根据
props和state计算出要返回的 JSX(必须是“纯函数”——只算结果,不做副作用)。React - 事件处理 :响应用户点击、输入等事件,去发请求、更新状态、跳转页面,这些都是“副作用”。React
但是有一类事情比较尴尬: 它不是用户点了某个按钮才发生,而是 “只要组件出现在屏幕上,就应该发生” 。 比如:连接聊天室、播放视频、注册浏览器事件监听、埋点统计…… 这时候就轮到 Effect 上场了。
例子:用 Effect 控制 <video> 播放
假设我们有一个简单的视频播放器组件,希望通过 isPlaying 控制它是「播放」还是「暂停」:
import { useEffect, useRef } from "react";
function VideoPlayer({ src, isPlaying }) {
const videoRef = useRef(null);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
if (isPlaying) {
video.play();
} else {
video.pause();
}
}, [isPlaying]); // 只有 isPlaying 变化时才重新同步
return <video ref={videoRef} src={src} loop playsInline />;
}
外层再包一层按钮切换状态:
import { useState } from "react";
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
return (
<>
<button onClick={() => setIsPlaying(p => !p)}>
{isPlaying ? "暂停" : "播放"}
</button>
<VideoPlayer
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
isPlaying={isPlaying}
/>
</>
);
}
这里发生了几件事:
- 组件渲染时,只是返回了一个带
ref的<video>标签 —— 渲染本身不碰 DOM API 。 - React 把这段 JSX “提交(commit)”到真实 DOM 上。
- 提交结束后,React 才调用
useEffect里的回调,在那里我们用video.play()/video.pause()去操作真实 DOM。React
用一句话总结:
Effect = 渲染之后,用当前的 props/state 去“同步”外部系统的状态。
为什么需要依赖数组?
如果不给 useEffect 传依赖数组:
useEffect(() => {
// ...
});
那它会在 每一次渲染后都执行 。有时这会:
- 性能不好:比如每次输入都重新连一次服务器。React
- 行为错误:比如每次渲染都重新触发一次「淡入动画」。
所以我们通常会像上面的例子一样传入依赖数组:
useEffect(() => {
// 根据 isPlaying 同步视频状态
}, [isPlaying]);
含义是: 只有当 isPlaying 发生变化时,才需要再次运行这个 Effect 。
一个经典坑:在 Effect 里直接 setState
看下面这段“反例”代码(简化自文档):React
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});
发生了什么?
- 组件渲染完,Effect 运行,
setCount(count + 1)。 - 状态变了,又触发一次渲染。
- 渲染完又运行 Effect,再次
setCount…… - 无限循环 🔄,页面直接炸掉。
记住一条经验:
Effect 更适合“和外部世界同步”,而不是在内部“纯粹改 state”。 如果只是想根据一个 state 推出另一个 state,通常可以用计算属性或在事件里处理,而不是上来就写 Effect。React
10. React 生命周期
在 React 中,我们通常熟悉组件的生命周期:组件会挂载(mount)、更新(update)、卸载(unmount)。但当你使用 React Hooks 中的 useEffect、或其他 “副作用” 时,其实需要换一种视角来看 — 每一个 Effect(副作用)本身也有自己的“同步启动/停止”过程 。文档指出:
“Effects have a different lifecycle from components… An Effect can only do two things: to start synchronizing something, and later to stop synchronizing it.” React 也就是说:我们不再单纯去想“组件何时挂载/更新/卸载”,而是去想:“这个 Effect 是什么时候开始同步?什么时候停止同步?”。
核心示例:ChatRoom
文档中给了一个非常直观的示例:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
return <h1>Welcome to the {roomId} room!</h1>;
}
这个例子帮助我们理解 Effect 的“启动”(connect)与“清理/停止”(disconnect)机制。 React
为什么不仅仅是“挂载/卸载”?
假设用户最初进入 "general" room,然后切换到 "travel" room。组件并没有卸载——只是 roomId 这个 prop 发生了变化。那 Effect 应该怎么做?
- 停止同步旧的 roomId (断开 “general” 连接)
- 启动同步新的 roomId (连接 “travel”) 文档里描述:
“At this point, you want React to do two things: 1. Stop synchronizing with the old
roomId… 2. Start synchronizing with the newroomId.” React 这正说明:Effect 的同步并非只在挂载或卸载时触发一次,而可能在组件仍然挂载的状态下反复 “停止旧的→启动新的”。
从 Effect 角度思考
文档强调一种思考方式:
“Instead, always focus on a single start/stop cycle at a time. … All you need to do is to describe how to start synchronization and how to stop it.” React 也就是说:你写
useEffect时,别总想着 “组件什么时候挂载/更新/卸载”。而直接问自己两个问题:
- 当这个 Effect 启动(synchronize)时,我该做什么?
- 当这个 Effect 停止(cleanup)时,我该做什么?
在上面 ChatRoom 的例子里:
- 启动 →
createConnection(...).connect() - 停止 →
connection.disconnect()
无论是因为 roomId 变了,还是组件卸载了,这两个流程都要被正确执行。
依赖数组(Dependencies)要点
Effect 的行为关键还在于依赖数组:[] 或 [roomId] 等。文档指出:
如果你在 Effect 内读取了一个可能随渲染改变的值(即“reactive value”),就必须把它放进依赖数组。 React+1
在前例中,
roomId是 prop,会变,因此放[roomId]。而serverUrl = 'https://localhost:1234'是一个固定常量,不会变,因此不必放进数组。 React如果你把所有用到的 reactive 值都“移出”组件或 effect 内部(即变成常量),那就可以用空数组
[],表示“只启动一次、停止一次”。例如:const serverUrl = 'https://localhost:1234'; const roomId = 'general'; function ChatRoom() { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, []); return <h1>Welcome to the {roomId} room!</h1>; }
小结
- 每个 Effect 是一个“同步过程”:启动→运行→停止,而不是简单绑定组件生命周期。
- 当你写
useEffect(() => { … }, [deps])时,要思考 “我什么时候启动”“我什么时候停止”这个同步。 - 依赖数组的每一个值都必须是你在 Effect 内读取的、且可能变的 reactive 值。漏掉就容易出 bug。
- 用空依赖(
[])意味着 “组件挂载时启动,同卸载时停止;中间不随 prop/state 变动而重新同步”。 - 用非空依赖(如
[roomId])意味着 “只要某个关键值变了,就停止旧同步、启动新同步”。
11. Hook
写 React 的时候,你经常会遇到“两个地方做了同一件事”的情况:有一段 state + handler + JSX 逻辑,在一个组件里出现两次,或者在多个组件里重复。官方的建议是:把 可复用的、有状态的逻辑 抽成自定义 Hook。注意这里的关键词是“逻辑”,不是“状态”——每次调用 Hook 都会创建自己独立的一份 state。 React
例子:把表单输入的重复逻辑抽出来
先看一个没有自定义 Hook 的 Form 组件:它对 firstName/lastName 两个输入框分别维护 state,并写两个几乎一样的 onChange 处理函数。
import { useState } from "react";
export default function Form() {
const [firstName, setFirstName] = useState("Mary");
const [lastName, setLastName] = useState("Poppins");
return (
<>
<label>
First name:
<input
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
/>
</label>
<label>
Last name:
<input
value={lastName}
onChange={(e) => setLastName(e.target.value)}
/>
</label>
<p><b>Good morning, {firstName} {lastName}.</b></p>
</>
);
}
重复点很明显:
- 每个字段都要一份 state
- 每个字段都要一个 change handler
- 每个 input 都要写
value和onChange
于是我们可以提炼出一个 useFormInput:
import { useState } from "react";
export function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
function handleChange(e) {
setValue(e.target.value);
}
return {
value,
onChange: handleChange,
};
}
然后 Form 变得更干净:我们只声明“我要两个输入框”,至于输入框怎么同步 state,由 Hook 内部处理。
import { useFormInput } from "./useFormInput";
export default function Form() {
const firstNameProps = useFormInput("Mary");
const lastNameProps = useFormInput("Poppins");
return (
<>
<label>
First name:
<input {...firstNameProps} />
</label>
<label>
Last name:
<input {...lastNameProps} />
</label>
<p><b>Good morning, {firstNameProps.value} {lastNameProps.value}.</b></p>
</>
);
}
这里最重要的点:
useFormInput只写了一份逻辑Form调了两次useFormInput- 所以得到的是两份 互不影响 的 state(firstName 的输入不会改 lastName)。
这正是“复用逻辑,不复用状态”的含义。 React
小结
当你发现组件里出现可重复的 stateful 逻辑时:
- 先确认它确实包含 Hook(比如
useState/useEffect) - 再把重复部分抽成
useXxx - 让组件只表达意图,逻辑细节交给 Hook
自定义 Hook 的价值就在于:让你的组件更像“声明式的 UI”,而不是“到处散落着重复的实现细节”。