关于Hook
理解hook前需要理解「渲染」
「 刷新页面 」 = 组件被卸载并重新挂载
「 渲染 」 = React 调用组件函数,将需要显示的UI展示在页面上
React的渲染过程:
- 执行组件函数 - 这个过程就是 渲染过程
- 生成虚拟DOM
- 把结果更新到真实页面(DOM) - diff比较
什么时候会触发渲染
- 第一次进入页面(组件挂载)
- state 变化
- props 变化
- 父组件渲染导致子组件也渲染
什么时候会卸载组件
- 条件渲染中不再渲染它
- 离开路由页面
- 手动卸载
useEffect
useEffect 用于处理组件渲染后产生的副作用(Side Effects),例如:
- 数据请求(fetch)
- DOM 操作
- 事件监听
- 定时器
- 订阅 / 取消订阅
- 日志 / 埋点
React 会在渲染后执行 effect,并在下次执行前或组件卸载时执行返回的清理函数(cleanup)。
useEffect(() => {
// 副作用逻辑:初始化、请求数据、注册一次性事件
return () => {
// 清理逻辑:组件卸载时执行
};
}, []); // 空依赖:只在组件挂载后执行一次
useEffect(() => {
// 当依赖项变化时执行
return () => {
// 下次依赖变化前清理
};
}, [a, b]);
useEffect(() => {
// 每次渲染都会执行
return () => {
// 下次渲染前清理
};
});第一次渲染完 → effect 执行 →(更新时)先执行 cleanup → 再执行新的 effect → (卸载时)执行最后一次 cleanup,避免:
- 内存泄漏
- 重复注册监听事件
- 重复计时器
- 重复订阅
- 使用过期状态
useRef
const ref = useRef(null)
<input ref={ref} />主要是一些对DOM的操作,更新它不会触发组件重渲染
应用场景:
- 聚焦
- 滚动
- 选择文本 / 控制视频 / 调用原始 DOM API
useMemo
const value = useMemo(() => {
return heavyWork(a, b)
}, [a, b])- 缓存“返回值”(通常是昂贵运算的结果)
- 避免组件每次渲染都重复执行计算
- 有时用于缓存“引用类型”,避免子组件不必要更新
应用场景:
| 场景 | 示例 |
|---|---|
| 计算量大 | 复杂列表过滤、排序 |
| 依赖数据不变的情况下避免重新计算 | 价格合计、统计逻辑 |
| 缓存对象/数组,避免引用频繁变化 | 传给子组件的 options、config |
useCallback
const handleClick = useCallback(() => {
doSomething(id)
}, [id])主要作用
- 缓存函数引用,让每次渲染不会重新创建新函数
- 搭配
React.memo使用时,可以减少子组件重渲染
应用场景:
| 场景 | 原因 |
|---|---|
| 传给 memo 子组件的事件处理函数 | 保持 props 引用稳定 |
| 使用依赖函数的 useEffect | 避免 effect 不必要触发 |
| 避免在循环中重复创建函数 | 性能优化 |
React.memo
React 函数组件的默认规则:父组件每次渲染时,子组件也会一起渲染(无论 props 有没有变化),会带来一些性能上的问题。
const MyComponent = React.memo(function MyComponent(props) {
console.log("render");
return <div>{props.value}</div>;
});React.memo 是一个高阶组件,用来缓存“函数组件”的渲染结果,避免不必要的重新渲染。一般都是搭配useCallback使用。
React.memo 与 useCallback 的关系
React.memo 只比较 props 的“浅层比较”
- 数字、字符串、布尔:没问题
- 对象、数组、函数:每次都是新引用 → props 判定为“变化”
例如:
<Child onClick={() => {}} />每次父组件渲染都会创建新函数→ React.memo 判定 props 改变→Child 会重新渲染→ React.memo 白用了
解决方法:
用 useCallback 稳定函数引用:
const handleClick = useCallback(() => {}, [])
<Child onClick={handleClick} />useContext
用来实现跨组件通信,避免层层传递props
- 创建context: Context + useState
import { createContext, useContext, useState, useEffect } from "react";
const UserContext = createContext(null);
//a. 创建相关Provider
export function UserProvider({ children }) {
const [user, setUser] = useState(null); // 后端数据
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// 获取数据函数
async function fetchUser() {
try {
setLoading(true);
const res = await fetch("/api/user");
if (!res.ok) throw new Error("Failed to fetch user");
const data = await res.json();
setUser(data);
setError(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
// 页面加载自动请求
useEffect(() => {
fetchUser();
}, []);
// 暴露给外部的操作
const refresh = () => fetchUser();
//🌟: 这里也可以使用useMemo来避免 value 频繁变化
return (
<UserContext.Provider value={{ user, loading, error, refresh }}>
{children}
</UserContext.Provider>
);
}
//b. 导出context
export function useUser() {
return useContext(UserContext);
}- 用provider包裹子组件
import { UserProvider } from "./UserContext";
import UserProfile from "./UserProfile";
export default function App() {
return (
<UserProvider>
<UserProfile />
</UserProvider>
);
}- 组件使用
import { useUser } from "./UserContext";
export default function UserProfile() {
const { user, loading, error, refresh } = useUser();
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
<h2>User Profile</h2>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
<button onClick={refresh}>Refresh</button>
</div>
);
}主要运用场景是:
- 用户状态(User)
- 登录用户信息
- 权限 role
- Token/登录态
- 全局 UI 状态
- 主题(dark / light)
- 当前语言 locale(i18n)
- 模式切换(侧边栏展开/关闭)
- 全局配置(Config)
- 请求配置(baseURL)
- App 配置
- 静态配置(env)
useReducer
这个目前我在项目里用得比较少(水平有限),先结合 (React文档):[https://react.dev/learn/extracting-state-logic-into-a-reducer]整理在这里
为什么不是 useState?
当 state 很简单(1~3 个字段),useState 最好。
但当出现以下情况时,用 useState 就“乱了”:
- 多个 state 之间存在关联逻辑
- 多个 setState 会导致难以维护
- 更新逻辑有明确定义的分支
- 需要将 state 更新逻辑抽离成一个可复用模块
比如:
- 表单状态(多个字段 + 多种操作)
- 复杂 UI 状态(modal、loading、errors、steps)
- 游戏状态 / 编辑器状态(撤销/重做)
- 全局状态管理(结合 useContext)
useReducer 用于管理复杂、多分支、可预测的状态更新逻辑,用来分离简化使用useState的状态管理逻辑,个人感觉其实就是让状态更新逻辑更像「状态机」
function reducer(state, action) {
switch (action.type) {
case "add":
return { count: state.count + 1 }
case "sub":
return { count: state.count - 1 }
default:
return state
}
}
function App() {
const [state, dispatch] = useReducer(reducer, { count: 0 })
return (
<>
<button onClick={() => dispatch({ type: "add" })}>+</button>
<button onClick={() => dispatch({ type: "sub" })}>-</button>
<p>{state.count}</p>
</>
)
}主要场景:
- 复杂表单管理
- 组件中包含多个状态且相互关联
- 多步骤流程
相关面试题自检 - AI
🔥 高频 ⭐⭐⭐⭐⭐
需要你完全掌握:
-
为什么 useEffect 会执行两次?(严格模式)
因为 React18 的 StrictMode 会故意执行两次“初始化阶段的副作用”,用于帮助开发者发现:
- 副作用是否是“纯函数”
- 是否存在未清理的订阅、定时器、事件监听
- 是否依赖同步状态更新导致的 bug
生产环境不会执行两次,只在开发环境中执行。
严格模式做的事:
- mount → 执行 effect( )
- unmount → 执行 cleanup( )(模拟一次卸载)
- mount → 再次执行 effect( )
-
React.memo 原理
React.memo 会缓存一个组件的渲染结果,并在下次渲染时对比新的 props:
- 如果 props“浅比较”后未变化 → 直接复用缓存 → 不重新渲染
- 如果 props 变化 → 执行重新渲染
浅比较规则:
- 基本类型直接比较值
- 引用类型比较地址(导致对象/数组/函数永远不相等)
-
useCallback 什么时候会“反而拖慢性能”?
useCallback 本身也需要开销(记忆 + 比较依赖项),
如果以下情况使用 useCallback,性能反而更差:
- 函数本身是轻量的
- 组件不会频繁重新渲染
- 不会传递给 React.memo 子组件
- 不作为 useEffect 的依赖
面试金句:
useCallback 是性能优化工具,不是默认写法。多数情况下,直接写函数更快。
-
为什么说 useRef 可以解决 stale closure(闭包陷阱)?
先理解 stale closure 是什么?
当你在 useEffect 内使用旧的 state 值时,就会出现“闭包捕获旧值”。
示例:
useEffect(() => { setInterval(() => { console.log(count); // 永远是旧的 count }, 1000); }, []);useRef 为什么能解决?
因为 useRef 永远指向同一个对象,改变
.current不会触发重新渲染。所以在 interval 中使用:
const countRef = useRef(0); useEffect(() => { const id = setInterval(() => { console.log(countRef.current); // 永远是最新值 }, 1000); return () => clearInterval(id); }, []); useEffect(() => { countRef.current = count; }, [count]);金句:
useRef 让我们绕过闭包捕获机制,可以读到“最新”的值。
-
useReducer vs useState 什么时候用哪个?
⭐ useState 用于:
- 状态简单(1~3 个字段)
- 状态结构不复杂
- 更新方式单一
⭐ useReducer 用于:
- 状态多、复杂(多字段)
- 多个状态之间存在关联(例如互相影响)
- 更新逻辑需要 switch/多分支
- 希望将“状态更新逻辑”抽离(更易维护)
- 需要支持撤销/重做(编辑器类)
金句:
当状态像“状态机”时,用 useReducer。
-
useMemo 为什么不能乱用?
useMemo 也有成本,例如:
- 需要执行依赖比较(一次浅比较)
- 需要缓存内存
- 需要维护闭包
如果你的计算很简单,或者依赖变化频繁:
👉 使用 useMemo 比直接计算更慢!
真正适合 useMemo 的情况:
- “重计算”开销大(排序、过滤、大数组计算)
- 依赖不常变
- 传给 React.memo 子组件(引用要稳定)
金句:
useMemo 是为了优化“昂贵计算”,不是为了所有计算
-
Context 为什么不适合“全局大对象”?
因为 Context 的 value 一旦变化,会导致所有消费它的组件全部重新渲染(无法避免)。
如果 value 是一个“大对象”,例如:
value={{ user, token, theme, config, cart, language, setting... }}只要其中任何一个字段变化:
👉 全部依赖 useContext 的组件都会全部重新渲染
👉 导致性能雪崩
金句:
Context 适合 “低频变化 + 小数据” ,不适合大而频繁变动的全局状态。
-
父组件渲染一定会导致子组件渲染吗?哪些情况下不会?
父组件渲染 默认会 导致子组件渲染,但有 三个例外:
1. 使用
React.memo缓存子组件只要 props 不变,子组件不会渲染。
2. 子组件是“稳定引用的 children”
例如:
<MemoChild> <StaticContent /> </MemoChild>只要 children 不变,不会重新渲染。
3. 子组件使用 useMemo 包装生成的 JSX
(虽然不推荐滥用)
金句:
父变 → 子不一定变;配合 memo / 稳定引用可以阻止渲染。