文章为个人学习总结,如有错误,请留言,谢谢
实习日记:
入职新实习公司快一个月了OvO,接触到很多新东西,之前都是写vue,现在改成写react,还是有些不习惯,日常开发使用hook比较多,现在来总结一下它们的注意事项
(一)useEffect
useEffect 是 React 中处理副作用(如数据获取、订阅、DOM 操作等)的核心 Hook。使用时需注意以下关键点,以避免常见问题(如无限循环、内存泄漏、依赖缺失等):
一.执行时机
useEffect 的回调函数会在 组件渲染完成后 执行,具体时机由「依赖数组」决定:
1.无依赖数组:useEffect(() => { ... })
- 组件每次渲染后都会执行(包括初始化挂载和后续更新)。
- 慎用!容易导致性能问题(如重复请求数据)或无限循环。
2.空依赖数组:useEffect(() => { ... }, [])
- 仅在组件首次挂载后执行一次(相当于类组件的
componentDidMount)- 适合执行一次性副作用(如初始化订阅、加载静态数据)。
3.有依赖数组:useEffect(() => { ... }, [dep1, dep2])
-
- 仅在「依赖项发生变化」时执行(包括初始化挂载时,依赖项首次赋值)。
- 核心原则:依赖数组必须包含回调中所有用到的外部变量(props、state、组件内定义的函数 / 变量)。
二.useEffect注意事项
1.依赖数组的「正确性」:避免缺失或冗余
必须包含所有外部依赖
如果回调中使用了某个变量,但未加入依赖数组,React 会警告(React Hook useEffect has a missing dependency),且可能导致逻辑错误:
原因:React 会缓存回调函数,若依赖项变化但未更新依赖数组,回调会使用旧的依赖值。
示例(错误):
const [count, setCount] = useState(0);
// 错误:回调使用了 count,但依赖数组为空
useEffect(() => {
console.log(\'Count:\', count); // 始终打印初始值 0
}, []);
示例(正确):
useEffect(() => {
console.log(\'Count:\', count); // count 变化时打印最新值
}, [count]); // 依赖数组包含 count
2. 避免冗余依赖
依赖数组中的变量若「不会变化」(如静态常量、组件外定义的函数),无需加入,否则会触发不必要的重执行:
// 组件外定义的静态函数(不会变化)
const fetchData = () => { /* ... */ };
function MyComponent() {
const [data, setData] = useState(null);
// 正确:fetchData 不会变化,无需加入依赖数组
useEffect(() => {
fetchData().then(res => setData(res));
}, []);
}
3.清理副作用:避免内存泄漏
useEffect 可以返回一个「清理函数」,用于在组件卸载或下一次副作用执行前,清理之前的副作用(如取消订阅、清除定时器、中止网络请求):
- 适用场景:订阅事件、定时器、网络请求、WebSocket 连接等。
示例(清理定时器):
useEffect(() => {
const timer = setInterval(() => {
console.log(\'定时器运行中...\');
}, 1000);
// 清理函数:组件卸载时清除定时器
return () => {
clearInterval(timer);
};
}, []); // 空依赖:仅挂载时创建定时器,卸载时清理
示例(取消网络请求):
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch(\'/api/data\', { signal })
.then(res => res.json())
.then(data => setData(data));
// 清理函数:组件卸载时中止请求
return () => {
controller.abort();
};
}, []);
注意:若 useEffect 没有清理函数,组件卸载后副作用可能继续运行(如定时器持续触发),导致内存泄漏。
4.避免无限循环
无限循环是 useEffect 最常见的问题,本质是「副作用触发组件更新,更新后又触发副作用」的死循环。常见原因及解决方案:
a.依赖数组缺失导致
回调中修改了某个 state,且该 state 未加入依赖数组,导致每次渲染都触发副作用:
const [count, setCount] = useState(0);
// 错误:回调修改了 count,但依赖数组为空,每次渲染都执行
useEffect(() => {
setCount(count + 1); // 无限触发更新
}, []);
解决方案:将修改的 state 加入依赖数组(若逻辑允许),或优化逻辑避免不必要的 state 更新。
b.依赖项是「引用类型」且每次渲染重新创建
若依赖项是对象、数组或函数(且在组件内定义),每次渲染会创建新的引用,导致 useEffect 误判为「依赖变化」,触发无限执行:
function MyComponent() {
// 每次渲染都会创建新的对象(引用变化)
const config = { url: \'/api/data\' };
useEffect(() => {
fetch(config.url).then(res => /* ... */);
}, [config]); // 错误:config 每次都是新引用,导致无限请求
}
解决方案:
- 用
useMemo缓存对象 / 数组(仅依赖变化时重新创建):
const config = useMemo(() => ({ url: \'/api/data\' }), []); // 空依赖:仅创建一次
- 用
useCallback缓存函数(避免每次渲染重新定义):
const fetchData = useCallback(() => {
fetch(\'/api/data\').then(res => /* ... */);
}, []); // 空依赖:仅创建一次
useEffect(() => {
fetchData();
}, [fetchData]); // 依赖缓存后的函数,不会频繁触发
5.useEffect 与 useLayoutEffect 的区别
useEffect 是异步执行(在浏览器绘制 UI 后执行),而 useLayoutEffect 是同步执行(在 DOM 更新后、浏览器绘制前执行)。使用时需注意:
- 优先用
useEffect:大多数场景(如数据获取、订阅)适合异步执行,避免阻塞 UI 绘制。- 仅在需要「立即操作 DOM」时用
useLayoutEffect:例如测量 DOM 元素尺寸、同步修改 DOM 样式(避免页面闪烁)。
示例(用 useLayoutEffect 测量 DOM 尺寸):
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
// 同步执行:DOM 已更新,可立即测量
setWidth(document.getElementById(\'box\').offsetWidth);
}, []); // 仅挂载时测量
6.其他注意事项
useEffect回调不能是异步函数:若需执行异步操作(如async/await),需在回调内部定义异步函数并调用:
useEffect(() => {
const fetchData = async () => {
const res = await fetch(\'/api/data\');
const data = await res.json();
setData(data);
};
fetchData(); // 调用异步函数
}, []);
- 避免在
useEffect中修改依赖项:若回调中修改了依赖数组中的变量,会触发下一次useEffect执行,可能导致循环。 - 依赖数组中的变量必须是「可比较的」:React 用
Object.is比较依赖项,若依赖项是复杂对象(如日期对象),需确保变化时引用也变化,或手动处理比较逻辑。 - 不要在
useEffect中直接调用setState修改依赖项:例如:
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 依赖 count,又修改 count,导致循环
}, [count]);
若需基于旧状态更新,用函数式更新(setCount(prev => prev + 1)),并避免将 count 加入依赖数组(若仅依赖旧状态):
useEffect(() => {
setCount(prev => prev + 1); // 函数式更新,不依赖外部 count
}, []); // 空依赖,仅执行一次
三.useEffect总结
使用 useEffect 的核心原则:
0.确保添加的依赖会变化,比如一个对象,那么他的引用可能会变化,不然这个依赖没用
useEffect依赖对象时,比较的是引用,不是值。- 要触发
useEffect,必须改变对象的引用(通过setState创建新对象)。- 如果只想监听对象内部的某个属性,直接依赖该属性即可。
- 深度比较可以使用自定义 Hook,但要注意性能开销。
- 明确依赖数组,确保包含所有用到的外部变量(避免缺失或冗余)。
- 必须清理副作用(如定时器、订阅),防止内存泄漏。
- 避免无限循环(注意引用类型依赖、不要在回调中修改依赖项)。
- 优先用
useEffect,仅在需要同步操作 DOM 时用useLayoutEffect。
(二)useRef的注意事项
useRef 是 React 中用于引用管理的核心 Hook,它能创建一个持久化的引用容器,在组件整个生命周期内保持不变
useRef 创建的 ref 对象在组件的整个生命周期中是持久存在的,它不会随着每次渲染而重新创建。它的 .current 属性可以随时修改,并且修改它不会触发重新渲染。
function MyInput() {
const inputRef = useRef(null); // 初始值通常设为 null
useEffect(() => {
// 组件挂载后,inputRef.current 才会指向真实的 DOM 元素
if (inputRef.current) {
inputRef.current.focus();
}
}, []); // 正确:传递 ref 对象
return <input ref={inputRef} type="text" />;
}
1. 修改 ref.current 不会触发重新渲染
这是 useRef 最重要的特性,也是最容易被误解的一点。
useRef 创建的是一个普通的 JavaScript 对象,它的 current 属性可以被随时修改。但是,这种修改不会像 setState 那样通知 React 组件状态发生了变化,因此不会触发组件的重新渲染。
2. ref.current 的值在渲染周期内是 “固定” 的
当组件开始一次渲染时,它会创建一个渲染快照,其中包含了当前所有变量和 ref.current 的值。在这次渲染过程中(包括在事件处理函数中),你访问到的 ref.current 都是这个快照中的值,而不是实时的值。
这一点在处理异步操作(如 setTimeout、Promise)或事件绑定时尤为重要。
3.总结:
能够长期存储资源的功能,记得及时清理,避免内存泄露
|
注意事项 |
描述 |
|
不触发渲染 |
修改 |
|
闭包中的快照 |
在一次渲染的闭包中, |
|
避免渲染期的读写操作 |
不在组件顶层(渲染期间)读写 ,应在副作用或事件处理中进行。 |
|
清理资源 |
当 |
(三)useMemo的注意事项
useMemo 是 React 中用于性能优化的核心 Hook,它通过缓存计算结果,避免组件每次渲染时重复执行昂贵的计算,从而提升应用响应速度
1. 只用于 “昂贵” 的计算
useMemo本身也有开销。它需要在每次渲染时比较依赖项数组,以决定是否需要重新计算。这个开销通常很小,但如果用于计算简单的操作(如a + b),那么useMemo带来的性能提升可能会被其自身的开销所抵消,甚至得不偿失。- 经验法则:仅当你确认某个计算确实是性能瓶颈时(例如,复杂的数组排序、大量数据的格式化、递归计算等),才使用
useMemo进行优化。
2. 正确指定依赖项数组
- 必须包含所有内部使用的外部变量:
useMemo的第二个参数是一个依赖项数组。useMemo会浅比较这个数组中的每个元素,只有当其中一个元素发生变化时,才会重新执行计算函数并返回新的值。你必须确保计算函数内部引用的所有外部变量(包括props、state、组件内定义的其他变量和函数)都被包含在这个依赖项数组中。- 遗漏依赖项的后果:如果遗漏了某个依赖项,
useMemo可能会返回基于旧依赖项计算出的 ** stale (过期) 值 **,导致难以预料的 bug。
function MyComponent({ a, b }) {
// 错误:计算函数使用了 a 和 b,但依赖项数组中只写了 [a]
const result = useMemo(() => {
return expensiveCalculation(a, b);
}, [a]); // 遗漏了 b
return <div>{result}</div>;
}
在上面的例子中,当 b 发生变化时,result 不会被重新计算,将一直保持旧值。
3.总结
|
注意事项 |
描述 |
|
只用于昂贵计算 |
避免对简单计算使用 |
|
正确指定依赖项 |
确保依赖项数组包含了计算函数中使用的所有外部变量。 |
|
空依赖数组的含义 |
空数组 |
|
不保证不重新计算 |
|
|
避免缓存 React 元素 |
缓存组件应使用 ,而非 |
(四)useCallback的注意事项
useCallback 是 React 中用于性能优化的重要 Hook,它通过缓存函数引用,避免不必要的组件重新渲染。然而,若使用不当,不仅无法优化性能,还可能引入额外开销或逻辑错误。
不必要重新渲染,是指?
由于父组件重新渲染导致函数 props (props在副组件被创建)被重新创建,进而引发的、即使数据没有实质变化的子组件重新渲染
通过 useCallback 缓存函数引用,并配合 React.memo 比较 props,我们可以精准地控制子组件只在其依赖的数据真正发生变化时才进行重新渲染,从而提升应用的性能。
1.明确使用场景:避免 “盲目缓存”
useCallback 的核心价值是 优化子组件的重渲染,而非所有函数都需要缓存。以下场景才值得使用:
- 子组件是
React.memo包裹的纯组件,且父组件传递给子组件的函数 props 是频繁创建的新实例(导致子组件不必要重渲染); - 函数作为
useEffect的依赖项,且希望仅在依赖变化时重新执行副作用(避免因函数实例变化导致useEffect频繁触发); - 函数传递给深层嵌套的子组件,且中间组件无优化,避免函数实例变化沿组件树传递,引发连锁重渲染
2.依赖项数组:必须完整且准确
3.与 React.memo 配合使用:缺一不可
useCallback 本身无法阻止子组件重渲染,它的作用是 让传递给子组件的函数引用保持稳定。要实现子组件优化,必须同时满足:
- 父组件用
useCallback缓存传递给子组件的函数; - 子组件用
React.memo包裹(React.memo会浅比较 props,若函数引用稳定,子组件不会因 props 变化重渲染)。
示例:正确配合使用
// 子组件:用 React.memo 包裹,浅比较 props
const Child = React.memo(({ fetchData }) => {
console.log('子组件渲染');
return <button onClick={fetchData}>请求数据</button>;
});
// 父组件:用 useCallback 缓存 fetchData
function Parent({ userId }) {
const fetchData = useCallback(() => {
console.log('请求数据:', userId);
}, [userId]);
return <Child fetchData={fetchData} />;
}
解析:当 userId 不变时,fetchData 引用稳定,Child 不会因 fetchData 变化重渲染;当 userId 变化时,fetchData 重新创建,Child 才会重渲染。
反例:仅用 useCallback 未用 React.memo
// 错误:子组件未用 React.memo,即使函数引用稳定,也会随父组件重渲染
const Child = ({ fetchData }) => {
console.log('子组件渲染');
return <button onClick={fetchData}>请求数据</button>;
};
function Parent({ userId }) {
const fetchData = useCallback(() => { /* ... */ }, [userId]);
return <Child fetchData={fetchData} />;
}
解析:父组件每次重渲染时,Child 都会随之重渲染,useCallback 的缓存失去意义。
4.总结
使用 useCallback 的核心是 “按需缓存”:
- 仅在需要优化子组件重渲染或稳定
useEffect依赖时使用; - 依赖项数组必须完整包含函数内所有外部变量;
- 必须与
React.memo配合使用才能实现子组件优化; - 避免缓存依赖频繁变化的函数或引用未缓存的不稳定函数;
- 区分
useCallback与useMemo的用途,不混淆使用。
|
注意事项 |
描述 |
|
只用于昂贵计算 |
避免对简单计算使用 |
|
正确指定依赖项 |
确保依赖项数组包含了计算函数中使用的所有外部变量。 |
|
空依赖数组的含义 |
空数组 |
|
不保证不重新计算 |
|
|
避免缓存 React 元素 |
缓存组件应使用 |
useCallback 与 useMemo 的区别:避免混淆
useCallback 和 useMemo 都是缓存工具,但缓存对象不同,需避免混淆:
|
特性 |
|
|
|
缓存对象 |
函数引用 |
计算结果(值) |
|
核心用途 |
优化子组件重渲染(配合 |
避免昂贵计算重复执行 |
|
依赖项变化时 |
返回新的函数实例 |
返回新的计算结果 |
(五)useReducer的注意事项
useReducer 是 React 中用于复杂状态管理的核心 Hook,它通过 reducer 函数统一处理状态更新逻辑,让状态变化更可预测、可维护。
1. reducer 必须是纯函数
- 核心原则:
reducer函数接收(state, action)并返回新状态,必须满足纯函数特性:
- 不修改原状态(
state是只读的,需返回新对象 / 数组);- 无副作用(不进行数据请求、DOM 操作、定时器等);
- 输入相同(
state+action)时,输出始终一致。
- 错误示例:直接修改原状态
// 错误:修改原 state(违反纯函数原则)
function reducer(state, action) {
if (action.type === 'ADD_ITEM') {
state.list.push(action.payload); // 直接修改原数组
return state; // 返回原状态,React 可能无法检测变化
}
return state;
}
2. 避免在 reducer 中依赖外部变量
3.初始状态的正确设置
当组件的 初始状态依赖于外部 props 或者需要进行动态计算 时,必须使用函数的形式 来传递初始值。(特殊设定)(其实就是缓存了第一次挂载的值,后面就算有更新,也会被忽略,如果每次都需要使用最新的值,最好使用函数式传递依赖)。
直接传递一个值(例如,一个对象字面量)作为初始状态。
错误做法
function MyComponent({ initialCount }) {
// 错误:当初始值依赖 props 时,直接传递对象
const initialState = { count: initialCount };
const [state, dispatch] = useReducer(reducer, initialState);
// ...
}
传递一个函数,该函数返回初始状态。
正确做法
function MyComponent({ initialCount }) {
// 正确:使用函数来动态计算初始状态
const [state, dispatch] = useReducer(
reducer,
() => ({ count: initialCount }) // 这个函数会在挂载时执行
);
// ...
}
原因:React 会把这个函数当作 “初始化函数”。它不会立即执行,而是在组件第一次挂载时才调用。这确保了在初始化状态时,使用的是挂载那一刻最新的 initialCount props 值。
4.dispatch 是稳定的引用(重点)
核心原则: useReducer 返回的 dispatch 函数引用在组件的整个生命周期中是稳定不变的。
为什么这很重要?有什么好处?
这主要是为了优化性能,特别是在配合 useEffect 和 React.memo 时。
让我们来看一个不使用 useReducer 的反例,来理解这个问题:
// 反例:没有 useReducer,状态逻辑复杂
function MyComponent() {
const [count, setCount] = useState(0);
const [data, setData] = useState(null);
// 这个 handleClick 函数在每次渲染时都会被重新创建
const handleClick = () => {
setCount(c => c + 1);
// ... 其他复杂逻辑
};
// 问题:因为 handleClick 每次都不一样,useEffect 会在每次渲染后都执行
useEffect(() => {
console.log('Effect triggered because handleClick changed');
// 假设这里有一个订阅或数据请求
}, [handleClick]); // 依赖了一个不稳定的函数
return <button onClick={handleClick}>Click me</button>;
}
问题分析:
- 每次组件渲染,
handleClick都是一个新的函数实例。 useEffect的依赖数组里包含handleClick。- 因此,每次渲染后,
useEffect都会认为依赖项变了,从而重新执行。这会导致不必要的网络请求、事件订阅等,造成性能问题。
现在,我们用 useReducer 来解决这个问题:
// 正例:使用 useReducer
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { count: 0, data: null });
// dispatch 函数是稳定的,不会在每次渲染时变化
useEffect(() => {
console.log('Effect triggered ONLY when component mounts');
const subscription = someEventSource.subscribe((newData) => {
// 使用稳定的 dispatch 来更新状态
dispatch({ type: 'UPDATE_DATA', payload: newData });
});
return () => subscription.unsubscribe();
}, [dispatch]); // 依赖一个稳定的 dispatch
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
</div>
);
}
(六)useContext的注意事项
useContext 是 React 中用于在函数组件中访问 Context 数据的核心 Hook,它提供了一种简洁的方式来共享全局数据(如主题、用户信息、权限等),而无需通过 props 层层传递
一. 核心作用
- 跨组件共享数据:避免
propsdrilling(Props 层层传递),尤其适用于深层嵌套的组件;- 简化状态管理:对于全局共享的简单状态(如主题、用户信息),无需使用
useReducer或 Redux,useContext即可满足需求;- 实时响应变化:当
Context的值发生变化时,所有使用useContext消费该Context的组件都会自动重新渲染。
二. 必须在 Context.Provider 范围内使用(或指定默认值)
- 若组件在
Context.Provider之外使用useContext,则会返回创建Context时指定的默认值(若未指定默认值,会报错);- 若需在
Provider之外使用组件,建议创建Context时指定合理的默认值。
示例:指定默认值
// 创建 Context 时指定默认值
const UserContext = React.createContext({ name: '匿名用户', id: -1 });
// 在 Provider 之外使用
function GuestUser() {
const user = useContext(UserContext);
return <p>欢迎,{user.name}</p>; // 输出:欢迎,匿名用户
}
三. useContext 会订阅整个 Context 值,需避免不必要的重渲染
- 当
Context的值发生变化时,所有使用useContext的组件都会重新渲染,即使组件只使用了Context中的部分数据;- 若
Context中包含多个不相关的数据(如用户信息、主题、权限),建议拆分Context,避免一个数据变化导致所有组件重渲染。
优化示例:拆分 Context
// 拆分多个独立的 Context
const UserContext = React.createContext();
const ThemeContext = React.createContext();
// 组件按需订阅
function UserProfile() {
const user = useContext(UserContext); // 只订阅用户信息
return <p>用户名:{user.name}</p>;
}
function ThemeButton() {
const theme = useContext(ThemeContext); // 只订阅主题
return <button style={{ background: theme }}>按钮</button>;
}
四. 避免在渲染期间创建 Context
- 若在组件内部创建
Context,每次组件渲染都会生成新的Context对象,导致子组件中useContext无法获取到正确的值(因为Context实例已变化);- 正确做法:在组件外部创建
Context,确保Context实例的稳定性。
错误示例:组件内部创建 Context
function Parent() {
// 错误:每次渲染都会创建新的 Context
const LocalContext = React.createContext();
return (
<LocalContext.Provider value="test">
<Child />
</LocalContext.Provider>
);
}
function Child() {
const value = useContext(LocalContext); // 可能获取到旧值或报错
return <p>{value}</p>;
}
五. useContext 不能在条件语句或嵌套函数中使用
- 和所有 React Hook 一样,
useContext必须在组件的顶层调用(即函数组件的直接作用域内),不能在if、for循环或嵌套函数中使用- 这是为了确保 React 能正确追踪 Hook 的调用顺序,避免渲染异常。
错误示例:条件语句中使用 useContext
function MyComponent() {
if (someCondition) {
// 错误:在条件语句中调用 useContext
const value = useContext(MyContext);
}
return <div></div>;
}
六. Context 的值更新机制
Context.Provider的value属性发生变化时(浅比较),所有消费该Context的组件都会重新渲染;- 若
value是对象或数组,建议使用useState或useReducer管理,避免直接修改原对象 / 数组(否则value的引用未变,组件不会重新渲染)。
示例:正确更新 Context 值
function App() {
// 使用 useState 管理 Context 值(对象类型)
const [user, setUser] = useState({ name: '张三', age: 20 });
const updateAge = () => {
// 正确:返回新对象,使 value 引用变化
setUser(prev => ({ ...prev, age: prev.age + 1 }));
};
return (
<UserContext.Provider value={user}>
<UserInfo />
<button onClick={updateAge}>增长年龄</button>
</UserContext.Provider>
);
}
七.总结
|
特性 |
作用 |
适用场景 |
|
跨组件共享数据 |
避免 Props drilling,共享全局数据 |
主题、用户信息、权限配置等全局状态 |
|
简化状态管理 |
无需复杂状态管理库,适用于简单全局状态 |
小型应用或全局共享的简单数据 |
|
实时响应变化 |
Context 值变化时自动重新渲染组件 |
需要根据全局状态动态更新 UI 的场景 |
|
需手动优化性能 |
拆分 Context 避免不必要的重渲染 |
Context 中包含多个不相关数据的场景 |
2015

被折叠的 条评论
为什么被折叠?



