React 常用技术知识全景:从组件到 Hooks 的系统理解
你写 React 时经常会遇到这些问题:为什么 setState 后马上读不到新值?为什么列表不能用数组下标当 key?为什么 useEffect 会重复执行?
这些问题不是孤立 API 细节,而是 React 的核心模型在真实代码里的表现。
如果能把组件、状态、渲染、Effect 和数据流串起来,React 代码会更容易维护,面试里的追问也能回答得更有条理。
本文面向已经会写基础 JSX、想系统整理 React 常用知识的前端开发者。文章以 React 19 文档中的当前推荐模型为参考,但大部分内容同样适用于 React 18 的函数组件和 Hooks 开发。
1. React 的核心:用组件描述 UI
React 的第一层心智模型是:UI 是组件树,组件是返回 JSX 的 JavaScript 函数。
jsx
export default function ProfileCard() {
return (
<article>
<h2>Ada Lovelace</h2>
<p>Frontend Engineer</p>
</article>
);
}
这个组件没有状态,也没有副作用。给定同样的输入,它应该返回同样的 UI 描述。
边界与失效场景:组件函数不要在渲染过程中修改外部变量、发请求、注册事件或直接操作 DOM。React 官方规则强调组件和 Hooks 应保持纯粹;与外部系统同步应放到事件处理函数或 Effect 中。
面试回答模板:
React 组件本质上是描述 UI 的函数。组件接收 props、读取 state,返回 JSX。React 根据组件返回的描述来计算需要展示的界面。
2. JSX:JavaScript 里的 UI 描述语法
JSX 不是 HTML 字符串,而是 JavaScript 的语法扩展。它让我们在 JavaScript 中描述 UI 结构。
jsx
const user = {
name: "Ada",
avatarUrl: "https://example.com/avatar.png",
};
export default function UserAvatar() {
return (
<img
src={user.avatarUrl}
alt={`${user.name}'s avatar`}
width={80}
height={80}
/>
);
}
JSX 中使用 {} 插入 JavaScript 表达式。属性名通常使用 DOM 对应的 React 写法,例如 className、htmlFor。
边界与失效场景:JSX 中只能放表达式,不能直接写 if 语句或 for 语句。需要条件渲染时,用提前 return、三元表达式或 &&;需要列表渲染时,用 map() 生成 JSX 数组。
3. props:父组件向子组件传递输入
props 是组件的外部输入。它适合表达"父组件决定,子组件消费"的数据。
jsx
function Badge({ tone, children }) {
return <span className={`badge badge-${tone}`}>{children}</span>;
}
export default function App() {
return <Badge tone="success">Published</Badge>;
}
children 是一种特殊 prop,代表组件标签中间包裹的内容。
边界与失效场景:子组件不要直接修改 props。props 代表父组件传入的快照,子组件需要变化时,应通过事件回调通知父组件更新状态。
jsx
function CounterButton({ count, onIncrement }) {
return (
<button type="button" onClick={onIncrement}>
Count: {count}
</button>
);
}
export default function CounterPanel() {
const [count, setCount] = React.useState(0);
return (
<CounterButton
count={count}
onIncrement={() => setCount((currentCount) => currentCount + 1)}
/>
);
}
边界与失效场景:上面的代码需要 React 在作用域中。如果使用现代 JSX transform,项目可不显式 import React from "react";为了让片段单独复制更清晰,也可以写成 import { useState } from "react"。
4. state:组件自己的记忆
state 是组件实例内部的记忆。状态变化会请求 React 重新渲染组件。
jsx
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button type="button" onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
useState(0) 返回当前状态和更新函数。点击按钮时调用 setCount,React 会安排一次重新渲染。
边界与失效场景:setCount 不会立刻改变当前这次渲染里的 count 变量。React 官方文档把 state 描述为"快照":一次渲染中的事件处理函数读到的是那次渲染对应的状态值。
5. State 是快照:为什么 setState 后马上读不到新值
看这段代码:
jsx
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log(count);
}
return (
<button type="button" onClick={handleClick}>
Count: {count}
</button>
);
}
点击按钮后,console.log(count) 打印的是当前渲染中的旧值。setCount 请求 React 用新值进行下一次渲染,不会修改当前闭包里的 count。
边界与失效场景:如果下一次状态依赖上一次状态,应使用函数式更新,避免闭包里的旧值影响计算。
jsx
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount((currentCount) => currentCount + 1);
setCount((currentCount) => currentCount + 1);
setCount((currentCount) => currentCount + 1);
}
return (
<button type="button" onClick={handleClick}>
Count: {count}
</button>
);
}
边界与失效场景:函数式更新适合"新状态依赖旧状态"的场景。如果新状态完全来自用户输入或接口返回值,可以直接传入新值。
6. 渲染与提交:React 如何把状态变化变成页面变化
React 更新 UI 可以理解成三个阶段:
- 触发渲染:组件首次挂载,或 state、props、context 变化。
- 渲染组件:React 调用组件函数,得到新的 JSX 描述。
- 提交更新:React 把必要变化应用到 DOM。
边界与失效场景:渲染阶段应该保持纯粹。不要在组件函数体里直接调用 setState、发请求或写 DOM;这些行为会让渲染过程不可预测。
调试信号:
- 在组件函数顶部写
console.log("render", props, state)观察渲染次数。 - 使用 React DevTools 查看组件树、props、state。
- 使用浏览器 Performance 面板观察长任务和提交阶段耗时。
7. 条件渲染:让 UI 跟随状态分支
React 中条件渲染本质上就是 JavaScript 条件表达式。
jsx
function EmptyState() {
return <p>No data yet.</p>;
}
function UserList({ users }) {
if (users.length === 0) {
return <EmptyState />;
}
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default function App() {
return <UserList users={[{ id: "u1", name: "Ada" }]} />;
}
边界与失效场景:如果使用 condition && <Component />,要确认 condition 是布尔值。0 && <Component /> 会渲染出 0,这在计数场景中会制造 UI 噪声。
8. 列表与 key:告诉 React 每一项是谁
渲染列表时,JSX 元素需要稳定的 key。
jsx
const todos = [
{ id: "t1", text: "Read React docs" },
{ id: "t2", text: "Write notes" },
];
export default function TodoList() {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
React 使用 key 判断数组中的每个组件对应哪条数据。当列表插入、删除、排序时,稳定的 key 能帮助 React 正确保留或重置对应项的状态。
边界与失效场景:不要在会增删排序的列表里使用数组下标作为 key。下标跟"位置"绑定,不跟"数据身份"绑定;顺序变化后,组件状态会跟着位置走,造成输入框内容错位等问题。
9. 表单:受控组件让状态成为唯一来源
受控组件把输入值交给 React state 管理。
jsx
import { useState } from "react";
export default function SearchBox() {
const [query, setQuery] = useState("");
function handleSubmit(event) {
event.preventDefault();
console.log("search:", query.trim());
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="query">Keyword</label>
<input
id="query"
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
<button type="submit">Search</button>
</form>
);
}
边界与失效场景:受控输入必须同时提供 value 和 onChange。如果只传 value 不更新状态,输入框会变成不可编辑。处理用户输入时要考虑空字符串、前后空格和提交时机。
10. 事件处理:事件里适合处理用户动作
事件处理函数用于响应用户交互,例如点击、输入、提交。
jsx
import { useState } from "react";
export default function LikeButton() {
const [liked, setLiked] = useState(false);
function handleClick() {
setLiked((currentLiked) => !currentLiked);
}
return (
<button type="button" aria-pressed={liked} onClick={handleClick}>
{liked ? "Liked" : "Like"}
</button>
);
}
边界与失效场景:事件处理函数不要写成 onClick={handleClick()},这样会在渲染时立即调用。正确写法是 onClick={handleClick} 或 onClick={() => handleClick(id)}。
11. Effect:用于同步 React 外部系统
useEffect 不是"组件加载后运行代码"的通用出口。React 官方文档把 Effect 定义为:让组件与 React 外部系统同步。
外部系统包括:
setInterval、clearInterval管理的 timer。window.addEventListener事件订阅。- WebSocket、第三方组件、地图、视频播放器。
- 需要与当前 props/state 同步的外部资源。
jsx
import { useEffect, useState } from "react";
export default function WindowWidth() {
const [width, setWidth] = useState(() => window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return <p>Window width: {width}</p>;
}
边界与失效场景:这段代码只能在浏览器环境运行,因为它读取了 window。如果用于服务端渲染或 React Server Components 环境,需要把访问 window 的逻辑限制在客户端组件或 Effect 中。
12. 不是所有逻辑都需要 Effect
如果一个值可以从 props 或 state 直接计算出来,就不要再放一份 state,也不要用 Effect 同步它。
不推荐:
jsx
import { useEffect, useState } from "react";
export default function FullName({ firstName, lastName }) {
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
return <p>{fullName}</p>;
}
推荐:
jsx
export default function FullName({ firstName, lastName }) {
const fullName = `${firstName} ${lastName}`;
return <p>{fullName}</p>;
}
边界与失效场景:Effect 适合"同步外部系统",不适合把可计算数据从一份 state 复制到另一份 state。多余的 Effect 会增加渲染次数和状态不一致的机会。
13. Effect 生命周期:同步、清理、重新同步
Effect 的生命周期不是"组件生命周期"的一一映射,而是围绕同步过程展开:
- 组件渲染后,React 运行 Effect。
- 依赖变化时,React 先运行上一次 Effect 的清理函数。
- React 再运行本次渲染对应的新 Effect。
- 组件卸载时,React 运行最后一次清理函数。
jsx
import { useEffect } from "react";
function createConnection(roomId) {
return {
connect() {
console.log("connect:", roomId);
},
disconnect() {
console.log("disconnect:", roomId);
},
};
}
export default function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
return <p>Room: {roomId}</p>;
}
边界与失效场景:依赖数组要包含 Effect 中读取的响应式值,例如 props、state 和组件内定义的变量。为了"少执行"而漏写依赖,会让 Effect 使用过期值。
调试信号:
- 在 Effect body 和 cleanup 中分别打印
console.log("connect", value)、console.log("cleanup", value)。 - 使用 React DevTools 检查 props/state 是否按预期变化。
- 排查重复订阅时,检查 cleanup 是否确实移除了旧订阅。
14. Hooks 规则:只能在顶层调用
Hooks 是 React 的特殊函数,调用位置有明确限制:
- 只能在组件或自定义 Hook 顶层调用。
- 不要在条件、循环、嵌套函数中调用 Hooks。
错误写法:
jsx
import { useState } from "react";
export default function Panel({ enabled }) {
if (enabled) {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
return <p>Disabled</p>;
}
推荐写法:
jsx
import { useState } from "react";
export default function Panel({ enabled }) {
const [count, setCount] = useState(0);
if (!enabled) {
return <p>Disabled</p>;
}
return (
<button type="button" onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
边界与失效场景:React 依赖 Hooks 的调用顺序来关联每个 Hook 的状态。条件调用会让不同渲染之间的 Hook 顺序不一致,导致状态错位。
15. 自定义 Hook:复用状态逻辑,而不是复用 UI
自定义 Hook 是以 use 开头的函数,用来复用状态逻辑。
jsx
import { useEffect, useState } from "react";
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(() => navigator.onLine);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
return isOnline;
}
export default function NetworkBadge() {
const isOnline = useOnlineStatus();
return <p>{isOnline ? "Online" : "Offline"}</p>;
}
边界与失效场景:自定义 Hook 复用的是逻辑,不共享状态。每次调用 useOnlineStatus() 的组件都会拥有自己的 Hook 状态和 Effect 同步过程。
16. 状态设计:把状态放在真正拥有它的地方
React 状态设计常见原则:
- 如果数据只影响一个组件,放在这个组件内部。
- 如果多个兄弟组件需要共享,提升到最近的共同父组件。
- 如果很多远距离组件需要共享,再考虑 Context 或状态库。
- 如果数据可以计算得到,不要额外存成 state。
边界与失效场景:状态提升会让父组件承担更多协调逻辑。把所有状态都放到全局会让数据流变得不透明,调试和复用成本上升。
17. 更新对象和数组:不要直接修改 state
React state 中的对象和数组应该按不可变方式更新。
jsx
import { useState } from "react";
export default function ProfileEditor() {
const [profile, setProfile] = useState({
name: "Ada",
city: "London",
});
function handleCityChange(event) {
setProfile((currentProfile) => ({
...currentProfile,
city: event.target.value,
}));
}
return (
<label>
City
<input value={profile.city} onChange={handleCityChange} />
</label>
);
}
边界与失效场景:不要写 profile.city = "Paris"; setProfile(profile);。这会复用同一个对象引用,React 和开发者都更难判断哪里发生了变化。
数组更新示例:
jsx
import { useState } from "react";
export default function TodoList() {
const [todos, setTodos] = useState([
{ id: "t1", text: "Read docs", done: false },
]);
function toggleTodo(id) {
setTodos((currentTodos) =>
currentTodos.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
}
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</label>
</li>
))}
</ul>
);
}
边界与失效场景:map、filter、展开运算符适合普通对象和数组的不可变更新。深层嵌套状态会让更新代码变复杂,应优先调整状态结构。
18. Context:解决跨层级传递,不替代所有状态管理
Context 适合传递主题、语言、当前用户、权限等跨层级数据。
jsx
import { createContext, useContext } from "react";
const ThemeContext = createContext("light");
function Toolbar() {
const theme = useContext(ThemeContext);
return <div data-theme={theme}>Toolbar theme: {theme}</div>;
}
export default function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
边界与失效场景:Context 更新会影响读取该 Context 的组件。高频变化的数据不要随意塞进一个巨大的 Context;可以按领域拆分 Context,或使用更适合高频更新的状态方案。
19. refs:保存不触发渲染的可变值
useRef 返回一个稳定对象,修改 ref.current 不会触发渲染。
jsx
import { useRef } from "react";
export default function FocusInput() {
const inputRef = useRef(null);
function handleFocus() {
inputRef.current?.focus();
}
return (
<>
<input ref={inputRef} />
<button type="button" onClick={handleFocus}>
Focus
</button>
</>
);
}
边界与失效场景:ref 适合保存 DOM 引用、timer id、上一次值等不参与渲染的数据。参与 UI 展示的数据应使用 state,否则界面不会随着值变化更新。
20. memo、useMemo、useCallback:只在有明确原因时使用
React 中常见的性能 API:
memo:让组件在 props 没变时跳过重新渲染。useMemo:缓存一次计算结果。useCallback:缓存函数引用。
jsx
import { memo, useMemo, useState } from "react";
const ProductList = memo(function ProductList({ products }) {
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
});
export default function ProductPage({ products }) {
const [keyword, setKeyword] = useState("");
const filteredProducts = useMemo(() => {
const normalizedKeyword = keyword.trim().toLowerCase();
if (normalizedKeyword.length === 0) {
return products;
}
return products.filter((product) =>
product.name.toLowerCase().includes(normalizedKeyword)
);
}, [keyword, products]);
return (
<>
<input
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
/>
<ProductList products={filteredProducts} />
</>
);
}
边界与失效场景:useMemo 不是语义保证,而是性能优化提示。只有计算成本、引用稳定性或子组件跳过渲染确实重要时再使用。没有测量或明确原因时,先保持代码简单。
21. 错误边界:捕获渲染阶段错误
错误边界用于捕获子组件渲染过程中的错误,并展示备用 UI。当前 React 官方文档仍使用 class component 定义错误边界。
jsx
import { Component } from "react";
export class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error("ErrorBoundary caught:", error, info);
}
render() {
if (this.state.hasError) {
return <p>Something went wrong.</p>;
}
return this.props.children;
}
}
边界与失效场景:错误边界不捕获事件处理函数里的错误。事件里的错误要用 try-catch 或统一异常处理逻辑处理。
22. React 技术知识调试清单
遇到 React 问题时,可以按这条线排查:
- UI 没更新:确认是否调用了 state setter,是否直接修改了对象或数组。
- 状态值旧:确认是否在当前渲染快照里读取 state,是否需要函数式更新。
- Effect 重复执行:确认依赖数组和 cleanup,检查开发环境 Strict Mode 下的额外检查。
- 列表状态错位:检查
key是否稳定,是否使用了数组下标。 - 组件重复渲染:用 React DevTools 和
console.log定位是谁触发更新。 - 表单不可输入:检查受控组件是否同时有
value和onChange。 - DOM 操作失效:检查 ref 是否拿到节点,操作是否发生在浏览器环境。
23. 面试回答模板
| 问题 | 回答重点 |
|---|---|
| React 组件是什么? | 返回 JSX 的 JavaScript 函数,用 props、state 和 context 描述 UI。 |
| props 和 state 区别? | props 是父组件传入的外部输入;state 是组件自己的记忆,更新会触发渲染。 |
| 为什么 state 更新后马上读不到新值? | state 是当前渲染的快照,setter 请求下一次渲染,不修改当前闭包变量。 |
| key 有什么作用? | key 标识列表项身份,帮助 React 在增删排序时匹配组件和数据。 |
| useEffect 适合做什么? | 同步 React 外部系统,例如事件订阅、timer、连接、第三方组件。 |
| 为什么 Hooks 不能写在条件里? | React 依赖 Hooks 调用顺序关联状态,条件调用会破坏顺序。 |
| ref 和 state 区别? | ref 可变但不触发渲染;state 更新会触发渲染,适合驱动 UI。 |
| Context 解决什么问题? | 解决跨层级读取共享数据的问题,不等于所有状态都该放全局。 |
总结
React 常用知识可以用一条主线串起来:组件负责描述 UI,props 传入外部输入,state 保存组件记忆,渲染把输入变成界面,Effect 负责同步外部系统,key 帮 React 识别列表项身份。
写 React 时,先保证组件纯粹、状态结构清晰、数据流单向,再根据测量结果处理性能问题。
面试回答也按这条线展开:定义概念,解释机制,给最小代码,再补充边界场景。
参考资料
- React Docs:Quick Start
- React Docs:Your First Component
- React Docs:State as a Snapshot
- React Docs:Render and Commit
- React Docs:Rendering Lists
- React Docs:Synchronizing with Effects
- React Docs:You Might Not Need an Effect
- React Docs:Lifecycle of Reactive Effects
- React Docs:Components and Hooks must be pure
- React Docs:React Versions