13518219792

建站动态

根据您的个性需求进行定制 先人一步 抢占小程序红利时代

ReactHooks在React-refresh模块热替换(HMR)下的异常行为

 什么是 react-refresh

react-refresh-webpack-plugin[1]是 React 官方提供的一个 模块热替换(HMR)插件。

10年的繁昌网站建设经验,针对设计、前端、开发、售后、文案、推广等六对一服务,响应快,48小时及时工作处理。营销型网站建设的优势是能够根据用户设备显示端的尺寸不同,自动调整繁昌建站的显示方式,使网站能够适用不同显示终端,在浏览器中调整网站的宽度,无论在任何一种浏览器上浏览网站,都能展现优雅布局与设计,从而大程度地提升浏览体验。创新互联从事“繁昌网站设计”,“繁昌网站推广”以来,每个客户项目都认真落实执行。

在开发环境编辑代码时,react-refresh 可以保持组件当前状态,仅仅变更编辑的部分。在umi[2]中可以通过 fastRefresh: {}快速开启该功能。

这张 gif 动图展示的是使用 react-refresh 特性的开发体验,可以看出,修改组件代码后,已经填写的用户名和密码保持不变,仅仅只有编辑的部分变更了。

react-refresh 的简单原理

对于 Class 类组件,react-refresh 会一律重新刷新(remount),已有的 state 会被重置。而对于函数组件,react-refresh 则会保留已有的 state。所以 react-refresh 对函数类组件体验会更好。本篇文章主要讲解 React Hooks 在 react-refresh 模式下的怪异行为,现在我来看下 react-refresh 对函数组件的工作机制。

在热更新时为了保持状态,useState 和 useRef 的值不会更新。

在热更新时,为了解决某些问题[3],useEffect、useCallback、useMemo 等会重新执行。

如上图所示,在文本修改之后,state保持不变,useEffect被重新执行了。

react-refresh 工作机制导致的问题

在上述工作机制下,会带来很多问题,接下来我会举几个具体的例子。

第一个问题

 
 
 
 
  1. import React, { useEffect, useState } from 'react';
  2. export default () => {
  3.   const [count, setState] = useState(0);
  4.   useEffect(() => {
  5.     setState(s => s + 1);
  6.   }, []);
  7.   return (
  8.     
  9.       {count}
  10.     
  •   )
  • }
  •  上面的代码很简单,在正常模式下,count值最大为 1。因为 useEffect 只会在初始化的时候执行一次。但在 react-refresh 模式下,每次热更新的时候,state 不变,但 useEffect 重新执行,就会导致 count 的值一直在递增。

    如上图所示,count 随着每一次热更新在递增。

    第二个问题

    如果你使用了ahooks[4]或者react-use[5]的 useUpdateEffect,在热更新模式下也会有不符合预期的行为。

     
     
     
     
    1. import React, { useEffect } from 'react';
    2. import useUpdateEffect from './useUpdateEffect';
    3. export default () => {
    4.   useEffect(() => {
    5.     console.log('执行了 useEffect');
    6.   }, []);
    7.   useUpdateEffect(() => {
    8.     console.log('执行了 useUpdateEffect');
    9.   }, []);
    10.   return (
    11.     
    12.       hello world
    13.     
  •   )
  • }
  •  useUpdateEffect 与 useEffect相比,它会忽略第一次执行,只有在 deps 变化时才会执行。以上代码的在正常模式下,useUpdateEffect 是永远不会执行的,因为 deps 是空数组,永远不会变化。但在 react-refresh 模式下,热更新时,useUpdateEffect 和 useEffect 同时执行了。

    造成这个问题的原因,就是 useUpdateEffect 用 ref 来记录了当前是不是第一次执行,见下面的代码。

     
     
     
     
    1. import { useEffect, useRef } from 'react';
    2. const useUpdateEffect: typeof useEffect = (effect, deps) => {
    3.   const isMounted = useRef(false);
    4.   useEffect(() => {
    5.     if (!isMounted.current) {
    6.       isMounted.current = true;
    7.     } else {
    8.       return effect();
    9.     }
    10.   }, deps);
    11. };
    12. export default useUpdateEffect;

    上面代码的关键在 isMounted

    初始化时,useEffect 执行,标记 isMounted 为 true

    热更新后,useEffect 重新执行了,此时 isMounted 为 true,就往下执行了

    第三个问题

    最初发现这个问题,是 ahooks 的 useRequest 在热更新后,loading 会一直为 true。经过分析,原因就是使用 isUnmount ref 来标记组件是否卸载。

     
     
     
     
    1. import React, { useEffect, useState } from 'react';
    2. function getUsername() {
    3.   console.log('请求了')
    4.   return new Promise(resolve => {
    5.     setTimeout(() => {
    6.       resolve('test');
    7.     }, 1000);
    8.   });
    9. }
    10. export default function IndexPage() {
    11.   const isUnmount = React.useRef(false);
    12.   const [loading, setLoading] = useState(true);
    13.   useEffect(() => {
    14.     setLoading(true);
    15.     getUsername().then(() => {
    16.       if (isUnmount.current === false) {
    17.         setLoading(false);
    18.       }
    19.     });
    20.     return () => {
    21.       isUnmount.current = true;
    22.     }
    23.   }, []);
    24.   return loading ? 
      loading
       : 
      hello world
      ;
    25. }

     如上代码所示,在热更新时,isUnmount 变为了true,导致二次执行时,代码以为组件已经卸载了,不再响应异步操作。

    如何解决这些问题

    方案一

    第一个解决方案是从代码层面解决,也就是要求我们在写代码的时候,时时能想起来 react-refresh 模式下的怪异行为。比如 useUpdateEffect 我们就可以在初始化或者热替换时,将 isMounted ref 初始化掉。如下:

     
     
     
     
    1. import { useEffect, useRef } from 'react';
    2. const useUpdateEffect: typeof useEffect = (effect, deps) => {
    3.   const isMounted = useRef(false);
    4. +  useEffect(() => {
    5. +   isMounted.current = false;
    6. +  }, []);
    7.   
    8.   useEffect(() => {
    9.     if (!isMounted.current) {
    10.       isMounted.current = true;
    11.     } else {
    12.       return effect();
    13.     }
    14.   }, deps);
    15. };
    16. export default useUpdateEffect;

    这个方案对上面的问题二和三都是有效的。

    方案二

    根据官方文档[6],我们可以通过在文件中添加以下注释来解决这个问题。

     
     
     
     
    1. /* @refresh reset */

    添加这个问题后,每次热更新,都会 remount,也就是组件重新执行。useState 和 useRef 也会重置掉,也就不会出现上面的问题了。

    官方态度

    本来 React Hooks 已经有蛮多潜规则了,在使用 react-refresh 时,还有潜规则要注意。但官方回复说这是预期行为,见该issue[7]

    不管你晕没晕,反正我是晕了,╮(╯▽╰)╭。

    参考资料

    [1]react-refresh-webpack-plugin:

    https://github.com/pmmmwh/react-refresh-webpack-plugin

    [2]umi:

    https://umijs.org/zh-CN/docs/fast-refresh

    [3]为了解决某些问题:

    https://github.com/facebook/react/issues/21019#issuecomment-800650091

    [4]ahooks:

    https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useUpdateEffect/index.ts

    [5]react-use:

    https://github.com/streamich/react-use/blob/master/docs/useUpdateEffect.md

    [6]官方文档:

    https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/API.md#reset

    [7]issue:

    https://github.com/facebook/react/issues/21019


    当前名称:ReactHooks在React-refresh模块热替换(HMR)下的异常行为
    本文链接:http://cdbrznjsb.com/article/cocjjge.html

    其他资讯

    让你的专属顾问为你服务