Back to Hooks

React Hooks & react-refresh(HMR)

docs/guide/blog/hmr.en-US.md

3.9.710.2 KB
Original Source

React Hooks & react-refresh(HMR)

What is react-refresh?

react-refresh-webpack-plugin is a hot module replacement (HMR) plugin provided by React.

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

In the development, react-refresh can keep state in component, and only change the edited part. In umi, can enable this feature by config fastRefresh: {}.

This gif shows the development experience of using the react-refresh. After edit some code, the username and password that have been filled in remain unchanged, only the edited part has been changed.

Simple Principles of react-refresh

For the Class component, react-refresh are always refresh (remount), existing state will be reset. For function components, react-refresh retains the existing state. Therefore, react-refresh provides a better experience for function components.

This article mainly explains the weird behavior of React Hooks in react-refresh mode. Now let us look at the working mechanism of react-refresh on function components.

  • To maintain the state during hot replacement, the value of useState and useRef will not update.
  • During hot replacement, To avoid some problems, useEffectuseCallbackuseMemoRun will re-executed.

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.

As shown in the gif, after the text is modified, state remains unchanged and useEffect is executed again.

Problem caused by react-refresh

Under the above working mechanism, there will be many problems. Next, I will give a few specific examples.

First problem

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

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

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

  return <div>{count}</div>;
};

The above code is very simple. In normal mode, the maximum value of count is 1. Because useEffect will only be executed once during initialization.

But in the react-refresh mode, the state does not change every time it is hot updated, but the re-execution of useEffect will cause the value of count to keep increasing.

As shown in the gif, count increases with each hot replacement.

Second problem

If you used ahooks v2 or react-use useUpdateEffect will also have unexpected behavior in HMR.

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

export default () => {
  useEffect(() => {
    console.log('useEffect');
  }, []);

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

  return <div>hello world</div>;
};

Compared with useEffect, useUpdateEffect ignores the first execution and only executes when the deps changes. In the normal mode of the above code, useUpdateEffect will never be executed, because deps is an empty array and will never change. But in react-refresh mode, during HMR, useUpdateEffect and useEffect are executed at the same time.

The reason for this problem is that useUpdateEffect uses ref to record whether it is currently executed for the first time, see the code below.

javascript
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;

The key of the above code is isMounted.

  • During initialization, after the useEffect is executed, the isMounted is changed to true
  • After the HMR, when the useEffect is re-executing, because the isMounted is already true, so the whole effect is executed again.

Third problem

The first time discovered this problem is the useRequest of ahooks, after HMR, the loading would always be true. After an inspection, the reason is use the isUnmount ref to mark whether the component is unmount.

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

function getUsername() {
  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>;
}

As the code above, during the hot replacement, isUnmount.current becomes true, causing the code to think that the component has been unmounted during the second execution.

How to solve these problems

First solution

The first solution is to solve it from the code, that is, when we write code, we can always remember the weird behavior in react-refresh mode.

For example, with useUpdateEffect, we can initialize the isMounted ref during initialization or hot replacement. as follows:

diff
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;

This solution is effective for both questions 2 and 3 above.

Second solution

According to Official Document, we can solve this problem by adding the following comment in the file .

javascript
/* @refresh reset */

After adding this question, every hot replacement will remount, that is, the component will be re-executed. useState and useRef will also be reset, so the above problem will not occur.

Official attitude

There are already many unspoken rules for React Hooks. When using react-refresh, there are still unspoken rules to pay attention to. But the official reply stated that this is expected behavior, see the issue.

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