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


    请记住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(); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    学会归类这些状态。

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

    const [userInfo, setUserInfo] = useState({firstName,lastName,school,age,address
    });
    
    const [weather, setWeather] = useState();
    const [room, setRoom] = useState(); 
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

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

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

    🌰示例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 (
    .....
    ) }
    • 1
    • 2

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

    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 (
    .....
    ) }
    • 1
    • 2

    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 (
    setCount(visible =>!visible)}>click setText(e.target.value)} /> setColor(e.target.value)} />
    ) }
    • 1
    • 2

    如果你下载了 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 (
    setVisible(preVisible => !preVisible)}>click { textRef.current = e.target.value }} /> { colorRef.current = e.target.value } />
    ) }
    • 1
    • 2

    思考💡: 如果 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 ( setCount(c => c + 1)}>click)
    } 
    
    • 1
    • 2

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

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

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

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

    较好解决方式✌:

    解法一:在有延迟调用场景时,可以通过 ref 来解决闭包问题
    function Demo() {const countRef = useRef(0);useEffect(() => {const timer = setTimeout(() => {console.log(countRef.current)}, 3000);return () => {clearTimeout(timer);}}, [])return ( { countRef.current++ }}>click)
    } 
    
    • 1
    • 2
    解法二: 可能我们需要一个自定义的 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; 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    最终代码:

    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 (<>
    当前数量:{ count }
    { setCount(pre => pre+1) }}>click)} export default Demo;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    被该 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 
    {comment}
    }
    • 1
    • 2

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

    较好解决方式 ✌:

    export default function App({ userId }) {return (
    ) };function ProfilePage({ userId }) {const [comment, setComment] = useState('');return
    {comment}
    }
    • 1
    • 2
    • 3

    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
    • 2

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

    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);}// ...
    } 
    
    • 1
    • 2

    在渲染期间就改变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;// ...
    } 
    
    • 1
    • 2

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

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

    ---- react 新文档

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

    期望: 初始化页面的时候

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

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

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

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

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

    但这些可能会造成:

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

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

    较好解决方式 ✌:

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

    🌰示例4:解决竞态问题

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

    这里可能会有一个问题,当 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;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们简单的梳理一下,还是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 
    slower comp
    }
    • 1
    • 2
    • 3
    • 4
    // 父组件
    export default function Demo() {const [color, setColor] = useState('#eee');const handleChange = (e) => {setColor(e.target.value)}return (
    背景颜色:
    ); }
    • 1
    • 2
    • 3

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

    较好解决方式✌:

    function ExpensiveTree() {const nowTime = new Date()while (new Date() - nowTime < 500 ) {}return 
    slower comp
    } // 看这里👉:将 input 中所需内容下移,变成一个组件 function Form() {const [color, setColor] = useState('#eee');const handleChange = (e) => {setColor(e.target.value)}return (
    背景颜色:
    ) } export default function Demo() {return (
    ); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

    示例2 :提升组件

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

    我们改一下示例:

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

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

    较好解决方式✌:

    function ColorPicker({ children }) {let [color, setColor] = useState("red");return (
    { color }}>背景颜色: setColor(e.target.value)} />{children}
    ); } export default function Demo() {return (); }
    • 1
    • 2
    • 3
    • 4
    • 5

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

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

    useReducer(待续。。)

  • 相关阅读:
    案例分享 | 基于ETest平台开发某型DCS测试系统
    XSS线上靶场---prompt
    采用python中的opencv2的库来运用机器视觉移动物体
    nvcc编译器之编译内幕(chapter 2&3)
    一文get到SOLID原则的重点
    stm32(GD32,apm32),开优化后需要特别注意的地方
    阿里云 ACK One 多集群管理全面升级:多集群服务、多集群监控、两地三中心应用容灾
    【SQL刷题】Day4----SQL计算函数专项练习
    Python爬虫进阶:提升爬虫效率
    【Rust日报】2023-09-30 使用Rust做web抓取
  • 原文地址:https://blog.csdn.net/web2022050903/article/details/126584021