Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React Hooks 在 react-refresh 模块热替换(HMR)下的异常行为 #42

Open
brickspert opened this issue May 10, 2021 · 2 comments

Comments

@brickspert
Copy link
Owner

brickspert commented May 10, 2021

什么是 react-refresh

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

A Webpack plugin to enable "Fast Refresh" (also previously known as Hot Reloading) for React components.

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

fast-refresh.gif

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

react-refresh 的简单原理

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

  • 在热更新时为了保持状态,useStateuseRef 的值不会更新。
  • 在热更新时,为了解决某些问题useEffectuseCallbackuseMemo 等会重新执行。

When we update the code, we need to "clean up" the effects that hold onto past values (e.g. passed functions), and "setup" the new ones with updated values. Otherwise, the values used by your effect would be stale and "disagree" with value used in your rendering, which makes Fast Refresh much less useful and hurts the ability to have it work with chains of custom Hooks.

Kapture 2021-05-10 at 11.37.54.gif

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

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

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

第一个问题

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

export default () => {
  const [count, setState] = useState(0);

  useEffect(() => {
    setState(s => s + 1);
  }, []);

  return (
    <div>
      {count}
    </div>
  )
}

上面的代码很简单,在正常模式下,count值最大为 1。因为 useEffect 只会在初始化的时候执行一次。
但在 react-refresh 模式下,每次热更新的时候,state 不变,但 useEffect 重新执行,就会导致 count 的值一直在递增。

Kapture 2021-05-10 at 12.09.47.gif

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

第二个问题

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

import React, { useEffect } from 'react';
import useUpdateEffect from './useUpdateEffect';

export default () => {

  useEffect(() => {
    console.log('执行了 useEffect');
  }, []);

  useUpdateEffect(() => {
    console.log('执行了 useUpdateEffect');
  }, []);

  return (
    <div>
      hello world
    </div>
  )
}

useUpdateEffectuseEffect相比,它会忽略第一次执行,只有在 deps 变化时才会执行。以上代码的在正常模式下,useUpdateEffect 是永远不会执行的,因为 deps 是空数组,永远不会变化。
但在 react-refresh 模式下,热更新时,useUpdateEffectuseEffect 同时执行了。

Kapture 2021-05-10 at 12.26.19.gif

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

import { useEffect, useRef } from 'react';

const useUpdateEffect: typeof useEffect = (effect, deps) => {
  const isMounted = useRef(false);

  useEffect(() => {
    if (!isMounted.current) {
      isMounted.current = true;
    } else {
      return effect();
    }
  }, deps);
};

export default useUpdateEffect;

上面代码的关键在 isMounted

  • 初始化时,useEffect 执行,标记 isMountedtrue
  • 热更新后,useEffect 重新执行了,此时 isMountedtrue,就往下执行了

第三个问题

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

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

function getUsername() {
  console.log('请求了')
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('test');
    }, 1000);
  });
}

export default function IndexPage() {

  const isUnmount = React.useRef(false);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    getUsername().then(() => {
      if (isUnmount.current === false) {
        setLoading(false);
      }
    });
    return () => {
      isUnmount.current = true;
    }
  }, []);

  return loading ? <div>loading</div> : <div>hello world</div>;
}

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

如何解决这些问题

方案一

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

import { useEffect, useRef } from 'react';

const useUpdateEffect: typeof useEffect = (effect, deps) => {
  const isMounted = useRef(false);

+  useEffect(() => {
+  	isMounted.current = false;
+  }, []);
  
  useEffect(() => {
    if (!isMounted.current) {
      isMounted.current = true;
    } else {
      return effect();
    }
  }, deps);
};

export default useUpdateEffect;

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

方案二

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

/* @refresh reset */

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

官方态度

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

Effects are not exactly "mount"/"unmount" — they're more like "show"/"hide".

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

❤️感谢阅读

关注公众号「前端技术砖家」,拉你进交流群,大家一起共同交流和进步。

image

@brickspert brickspert changed the title React Hooks 在 react-refresh 模块热替换(HRM)下的异常行为 React Hooks 在 react-refresh 模块热替换(HMR)下的异常行为 May 10, 2021
@lixingjuan
Copy link

Hi, react项目加了react-refresh后,初次编译和热编译的速度都不太理想,大佬有什么好的办法解决么?
初次编译大概 7万多ms,
热编译约3000-6000ms, 但是 hot-update.json文件 有时候加载会有3-6s的情况

wecom-temp-ba000a6792a847d32b552afa20fc5376

@dpzxsm
Copy link

dpzxsm commented Jul 18, 2024

https://github.com/dpzxsm/react-refresh-hotfix 这个webpack扩展插件可以解决这个问题

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants