关于 React Hook 可能出现的使用误区总结

本文总结了React Hooks在useState、useRef、useEffect和useMemo等常见使用中的误区,并提供了最佳实践。例如,避免useState拆分过细,学会善用useRef解决闭包问题,理解useEffect的正确用法以避免竞态问题,以及合理使用useMemo优化性能。通过这些案例,有助于提升React应用的开发质量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

请记住react 的公式: UI = f(state) ,这很重要。

useState


🌰示例1:useState 拆分过细

const [firstName, setFirstName] = useState();
const [lastName, setLastName] = useState();
const [school, setSchool] = useState();
const [age, setAge] = useState();
const [address, setAddress] = useState();

const [weather, setWeather] = useState();
const [room, setRoom] = useState(); 

较好解决方式✌: 适当的合并 state

学会归类这些状态。

firstName, lastName 均是用户的信息,可以放在一个 useState 进行管理。

const [userInfo, setUserInfo] = useState({firstName,lastName,school,age,address
});

const [weather, setWeather] = useState();
const [room, setRoom] = useState(); 

注意🎈:useState 的 set 动作永远是 替换 值。(React class 中的 this.setState 是会进行 合并 的)

在进行变更用户的某个信息例如 年龄 的时候记得带上之前的值。

setUserInfo((prevInfo) => ({...prevInfo,age: newAge
})) 

🌰示例2:多个状态实则只是一个状态的变形

doneSource 、doingSource 是 source 转变的。

const SomeComponent = (props) => {const [source, setSource] = useState([{type: 'done', value: 1},{type: 'doing', value: 2},])const [doneSource, setDoneSource] = useState([])const [doingSource, setDoingSource] = useState([])useEffect(() => {setDoingSource(source.filter(item => item.type === 'doing'))setDoneSource(source.filter(item => item.type === 'done'))}, [source])return (<div> ..... </div>)
} 

较好解决方式✌: 当一个状态可以被另外一个状态计算出来的话就不要去声明

const SomeComponent = (props) => {const [source, setSource] = useState([{type: 'done', value: 1},{type: 'doing', value: 2},])// 这里的 useMemo 视实际情况添加,通常不是需要大量的计算 react 是不建议使用 useMemoconst doneSource = useMemo(()=> source.filter(item => item.type === 'done'), [source]);const doingSource = useMemo(()=> source.filter(item => item.type === 'doing'), [source]);return (<div> ..... </div>)
} 

useRef


🌰示例1:多余的依赖

期望: 只有当 visible 变化时,弹出 Message 。

其中 text 、color 分别控制 弹窗的文案、背景颜色

function Demo(props) {const [visible, setVisible] = useState(false);const [text, setText] = useState('');const [color, setColor] = useState('red');useEffect(() => {Message(visible, text, color);}, [visible]);return (<div><buttononClick={() => setCount(visible =>!visible)}>click</button><input value={text} onChange={e => setText(e.target.value)} /><input value={color} onChange={e => setColor(e.target.value)} /></div>)
} 

如果你下载了 eslint-plugin-react-hooks插件的话,你会发现这行代码出现警告。

于是你在 effect deps 增加了 textcolor 依赖。

于是出现了一个问题,当 textcolor 发生变化的也会上传数据, 这并不符合我们的目标。

较好解决方式✌:善用 useRef

textcolor 改变的时候不需要重新更新视图的时候,尝试使用 useRef 去替代。

使用 useRef 去替换useState

function Demo(props) {const [visible, setVisible] = useState(false);const textRef = useRef('');const colorRef = useRef('red');useEffect(() => {// 注意这里的 Message 内部接收的时候也要做处理// 不能直接传 textRef.current, 这样可能会导致 Message 无法接收到最新的值Message(visible, textRef, colorRef);}, [visible]);return (<div><buttononClick={() => setVisible(preVisible => !preVisible)}>click</button><input value={text} onChange={e => { textRef.current = e.target.value }} /><input value={color} onChange={e => { colorRef.current = e.target.value } /></div>)
} 

思考💡: 如果 text 、color 值需要在视图上进行渲染,如何进行设计? - useReducer。

🌰示例2:缺少依赖导致的闭包问题

永远不要欺骗 hook

期望: 当进入页面 3s 后,输出当前最新的 count

function Demo() {const [count, setCount] = useState(0);useEffect(() => {const timer = setTimeout(() => {console.log(count)}, 3000);return () => {clearTimeout(timer);}}, [])return (<buttononClick={() => setCount(c => c + 1)}>click</button>)
} 

同样,当我们拥有 eslint-plugin-react-hooks插件的时候还是会报缺少 count 的错误, 且该代码在 3s 内多次点击按钮, 还是会输出 0。

此时我们想到将 count 加入到依赖项。

 useEffect(() => {const timer = setTimeout(() => {console.log(count)}, 3000);return () => {clearTimeout(timer);}}, [count]) 

但是这样又会陷入一个怪圈,当我们在点击的时候会重新调用 useEffect 中的方法。 这样并没有达到我们的需求。

较好解决方式✌:

解法一:在有延迟调用场景时,可以通过 ref 来解决闭包问题
function Demo() {const countRef = useRef(0);useEffect(() => {const timer = setTimeout(() => {console.log(countRef.current)}, 3000);return () => {clearTimeout(timer);}}, [])return (<buttononClick={() => { countRef.current++ }}>click</button>)
} 
解法二: 可能我们需要一个自定义的 hook useEvent。

假设 count 值需要在进行视图展示,换句话说就是当 count 改变的时候会改变视图。

可能我们需要一个自定义的 hook useEvent

具体的内容,你可以查看 Dan 在社区发布的一篇文章 useEvent 🔗

useEvent 实现方式 :

import React, { useRef, useLayoutEffect, useCallback } from 'react';

function useEvent(handler) {const handlerRef = useRef();// 在 render 之前执行useLayoutEffect(() => {handlerRef.current = handler;});return useCallback((...args) => {const fn = handlerRef.current;return fn(...args);}, []); // 因为 ref 的地址不会发生变化,可以在依赖项中进行忽略(同理 setState 也是)
}
export default useEvent; 

最终代码:

import React, { useState, useEffect } from 'react';

import useEvent from '@/hooks/useEvent';

function Demo() {const [count, setCount] = useState(0)const consoleCount = useEvent(() => {console.log(count)})useEffect(() => {const timer = setTimeout(() => {consoleCount();}, 3000);return () => {clearTimeout(timer);}}, [])return (<><div>当前数量:{ count }</div><buttononClick={() => { setCount(pre => pre+1) }}>click</button></>)}

export default Demo; 

被该 useEvent 包裹的函数,拿到外部的 props 或者 state 永远是最新的值。

useEffect


请特别注意以下两点📢:

1.useEffect在开发调试阶段会运行两次。 React 18 最大的特性之一就是可以支持稳定的并发渲染,在实际开发中我们可以加上strickMode来开启严格模式来支持稳定的并发渲染,该模式下为了能够暴露出一些特定情况的 bug, react 会在开发模式下调用两次 useEffect

2.请不要将 useEffect 当做 watcher监听的方式来使用

如果想跟上 react 的技术更新这些真的很重要,提前去注意总是好的,也是为了以后更好的迭代项目做准备。

哪怕是现在的项目并没有开启该模式。

🌰示例1:props 改变的时候 重置 state

期望: 当 userId变化的时候,将 ProfilePage组件的评论状态(即组件全部状态)清空

export default function ProfilePage({ userId }) {const [comment, setComment] = useState('');useEffect(() => {setComment('');}, [userId]);return <div>{comment}</div>
} 

当前组件如果需要展现正确的值的时候,中间会更新一次 dom然后去运行 useEffect, 由于改变状态并再次进行渲染及其子组件,无疑增加了额外的一次渲染。

较好解决方式 ✌:

export default function App({ userId }) {return (<div><ProfilePage key={userId} userId={userId} /></div>)
};function ProfilePage({ userId }) {const [comment, setComment] = useState('');return <div>{comment}</div>
} 

userIdProfilePage组件 key 的标识,当 userId发生变化的时候,由于组件ProfilePagekey 不同 react 则会重新render该组件的, 状态不在复用而是重建, 你不用担心这样会新建 dom, react fiber会进行比较,是否选择复用缓存。

🌰示例2:state 依赖于 props

期望: 当 items变换的 只重置selection的状态

function List({ items }) {const [selection, setSelection] = useState(null);const [otherState, setOtherState] = useState(xxx);useEffect(() => {setSelection(null);}, [items]);// ...
} 

咋一看没有什么问题,让我们仔细看一下这段代码是如何运行的:

1.第一次render:当 items变化的时候: 整个组件重新运行,此时selection的值为旧值, 当组件更新 dom之后, 运行useEffect的函数,由于运行了 set函数,组件需要重新 render .
2.第二次 render, 此时的 selection内部的值是最新的值。

❓ 可是我们只是变化了一次 itemslist组件居然渲染了两次页面, 显然跟我们想的不一样。

较好解决方式 1✌:

function List({ items }) {const [selection, setSelection] = useState(null);const [otherState, setOtherState] = useState(xxx);const [prevItems, setPrevItems] = useState(items);if (items !== prevItems) {setPrevItems(items);setSelection(null);}// ...
} 

在渲染期间就改变selection

prevItems总是记录上一次的值, 判断是否发生了变化, 如果发生变化则重新更新 selection,并储存当前items, 保证在下次渲染的时候使用的是上一次的值。 由于在第一次 render,更新到真实dom树上的时候, selection值已经是最新的了, 整个组件则渲染完毕。

当然在这里你可能会想到使用 React.memo 可以自己去尝试下。

较好解决方式 2✌:

当然,在例子中我们也发现,其实 selection更多是取决于items,他严格来说是依赖于 props 的一个变量,大可不必作为一个新的状态存储在 List组件中。

function List({ items }) {const [otherState, setOtherState] = useState(xxx);const selection = items.find(item => item.id === selectedId) ?? null;// ...
} 

React 更推荐这种方式去处理:

不管你怎么做,根据 props 或其他状态调整状态会使你的数据流更难理解和调试。当你检查代码的时候应考虑是否可以 重置所有状态 或者 在渲染期间计算所有内容

---- react 新文档

🌰示例3:正确的请求方式

期望: 初始化页面的时候

function App() {useEffect(() => {loadDataFromLocalStorage();checkAuthToken();}, []);// ...
} 

但是可能在开发模式下会渲染两次 ,尝试做一次判断。

question: 为什么开发模式下会渲染两次?

简单的来说,大部分人在开发的过程中并不会注意到去清理 Effect,提前在开发阶段暴露问题给开发者。详见开发中初次渲染调用两次 useEffect 内函数。

🎈例如我在 useEffect去注册一个事件,但是我并没有 return 一个 清理该监听的事件的函数; 或者是 使用一些弹窗,而这个组件可能会为了防止开发人员多次调用而创建多个 portals,所以只允许实例化一次。

但这些可能会造成:

1.调用弹窗关闭后未清除弹窗组件导致二次调用报错。

2.当该绑定事件到达一定量,页面由于绑定的事件过多会对用户浏览流畅性产生一定影响。

较好解决方式 ✌:

function App() {useEffect(() => {if(!isInit) {loadDataFromLocalStorage();checkAuthToken();}}, []);// ...
} 

🌰示例4:解决竞态问题

function User({ query }) {const [data, setData] = useState(null);useEffect(() => {const res = await fetch(`https://xxx?${query}/`);setData(res.json());}, [query]);if (data) {return <div>{data.name}</div>;} return null;
} 

这里可能会有一个问题,当 query变化足够快的时候,可能会同时发出两个请求,而这两个请求可能返回时间的先后顺序与你预想不一致。举个例子:

假设我们希望查询 https://xxxx?id=123, 即 query 我们希望输入的是 123。但是由于 inputonChange函数在 input每次改变的时候都会改变 query 值, 输入间隔短倒一定时间的时候,输入12的时候与输入 123的请求同时发送。但是可能 123请求结果先返回, 后面12的请求结果后返回,则可能会造成 请求12的结果覆盖了我们希望得到的123的请求结果。

question: 什么是 竞态问题?

query变化足够快, 就会发起不同的请求,而展示哪个最终取决于最终返回的那个结果,这可能并不符合你的预期显示内容。

较好解决方式 ✌

加一个锁

function User({ query }) {const [page, setPage] = useState(1); const data = useData(`/api/search?${query}`);// ...
}

function useData(url) {const [data, setData] = useState(null);useEffect(() => {let ignore = false;fetch(url).then(res => {if (!ignore) {setData(res?.data || {});}});return () => {ignore = true;};}, [url]);return result;
} 

我们简单的梳理一下,还是queryid 为 123的例子。手速过快同时发送12, 123的请求,在每一次请求中,总是会执行上一次副作用的 cleanup 函数, 当发起123的请求的时候,请求 12的函数由于运行了cleanup函数, ignore变量已经被赋值为 true, 无论如何都没办法进入 setData 的操作, 这就解决了竞态竞争的问题。

或许你可以尝试使用一些市面上比较优秀的库: ahooksreact-usereact-QueryuseSWR。内部已经做了类似的判断。

useMemo


在遇到一些计算量大的时候我们总是会想到使用 useMemo的 hook,对计算内容进行缓存。

在进行函数优化的时候,也会想到使用 React.memo 中添加第二个函数来进行比较是否需要优化该函数。

但是在使用这些 hook 之前,可能你需要先考虑一下是否可以使用以下两种解决方式:

示例1 :向下移动 state:

// 子组件 
// 模拟组件重新渲染需要大量的计算
function ExpensiveTree() {const nowTime = new Date()while (new Date() - nowTime < 500 ) {}return <div> slower comp</div>
} 
// 父组件
export default function Demo() {const [color, setColor] = useState('#eee');const handleChange = (e) => {setColor(e.target.value)}return (<div>背景颜色: <input value={color} onChange={handleChange}/><ExpensiveTree /></div>);
} 

可以看到,每当我在 input框输入的时候都会重新渲染 ExpensiveTree,可能我们第一眼的直觉是不是只要将 ExpensiveTree组件包一层 useMemo不就好了吗,但是或许可以用另外一种的方式.

较好解决方式✌:

function ExpensiveTree() {const nowTime = new Date()while (new Date() - nowTime < 500 ) {}return <div> slower comp</div>
}

// 看这里👉:将 input 中所需内容下移,变成一个组件
function Form() {const [color, setColor] = useState('#eee');const handleChange = (e) => {setColor(e.target.value)}return (<div>背景颜色: <input value={color} onChange={handleChange}/></div>)
}

export default function Demo() {return (<div><Form /><ExpensiveTree /></div>);
} 

将原先会改变状态一部分内容给提取到一个组件中,与 ExpensiveTree形成并列关系。也就是将 state 状态下移了。 此时更新 state 的时候,只会重新render该子组件内部的内容。

示例2 :提升组件

🤔 但是如果父组件也依赖 input值呢, 即 state 上升到上层组件?

我们改一下示例:

// 子组件 
// 模拟组件重新渲染需要大量的计算
function ExpensiveTree() {const nowTime = new Date()while (new Date() - nowTime < 500 ) {}return <div> slower comp</div>
} 
// 父组件
export default function Demo() {const [color, setColor] = useState('#eee');const handleChange = (e) => {setColor(e.target.value)}return (
 👉 <div style={{ backgroundColor: color }} >背景颜色: <input value={color} onChange={handleChange}/><ExpensiveTree /></div>);
} 

此时上层 dom div的背景颜色需要 input的值来控制此时没办法使用刚才的方法了。但是真的只能用 useMemo了吗?

较好解决方式✌:

function ColorPicker({ children }) {let [color, setColor] = useState("red");return (<div style={{ color }}>背景颜色:<input value={color} onChange={(e) => setColor(e.target.value)} />{children}</div>);
}

export default function Demo() {return (<ColorPicker><ExpensiveTree /></ColorPicker>);
} 

demo组件根据是否关心 color状态来分为两个组件树 ColorPickerExpensiveTree。 而 ExpensiveTree通过children属性传递到 ColorPicker

这样的话从组件结构来看 ExpensiveTree 是否改变不取决于 ColorPicker 组件是否改变, 在改变 color 状态的时候,ColorPicker重新render的时候并不会导致内部 children重新 render,而是从缓存中复用(请注意,这里的缓存是 react 的双缓存树)。

useReducer(待续。。)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值