alibaba / hooks

A high-quality & reliable React Hooks library. https://ahooks.pages.dev/
https://ahooks.js.org/
MIT License
14.02k stars 2.71k forks source link

useHover 会导致强制render #2210

Closed lifegit closed 1 year ago

lifegit commented 1 year ago

直接上示例:

import {useHover} from 'ahooks';
import {ReactNode, useRef, useState} from 'react';
import {Button} from "antd";

const Counter = () => {
  const [state, setState] = useState(0);
  return (
    <div>
      <Button onClick={()=>setState(v=>v+1)}>+ {state}</Button>
    </div>
  );
};

const Index = () => {
  const ref = useRef() as React.MutableRefObject<HTMLDivElement>
  const isHovering = useHover(ref);
  console.log("isHovering", isHovering)

  const Action = ({children}: { children: ReactNode}) => (
    <div>
      {children}
    </div>
  );

  return (
    <div ref={ref}>
      <Action>
        <Counter />
      </Action>
    </div>
  );
};
export default Index;

现象

上述代码当点击button时,计数器正常增加,当移出鼠标时,计数器清零了(重新 render)。

解决方法

达成效果:鼠标移出计数器的值也不会改变。

-- 选择1: 移除 ref 或 useHover

...
const Index = () => {
  const ref = useRef() as React.MutableRefObject<HTMLDivElement>
  const isHovering = useHover(ref);
  console.log("isHovering", isHovering)

  return (
    <div>
      <Action>
        <Counter />
      </Action>
    </div>
  );

-- 选择2: Action 组件移动到外面

...
  const Action = ({children}: { children: ReactNode}) => (
    <div>
      {children}
    </div>
  );

const Index = () => {
  const ref = useRef() as React.MutableRefObject<HTMLDivElement>
  const isHovering = useHover(ref);
  console.log("isHovering", isHovering)

  return (
    <div ref={ref}>
      <Action>
        <Counter />
      </Action>
    </div>
  );

...
};

困惑

这很令人很痛苦困惑,看起来 react vdom diff 失败了,导致鼠标移出就 render,但只是监听一下鼠标, 这是为什么呢?

lifegit commented 1 year ago

令人费解的是,这样的方案是可以的:

const Index = () => {
  const ref = useRef() as React.MutableRefObject<HTMLDivElement>
  const isHovering = useHover(ref);
  console.log("isHovering", isHovering)

  return (
    <div ref={ref}>
       <div>
            <Counter />
        </div>
    </div>
  );
};
candy4290 commented 1 year ago

提供个有问题的最小复现链接吧

liuyib commented 1 year ago
import {useHover} from 'ahooks';
import {ReactNode, useRef, useState} from 'react';
import {Button} from "antd";

const Counter = () => {
  const [state, setState] = useState(0);
  return (
    <div>
      <Button onClick={()=>setState(v=>v+1)}>+ {state}</Button>
    </div>
  );
};

const Index = () => {
  const ref = useRef() as React.MutableRefObject<HTMLDivElement>
  const isHovering = useHover(ref);
  console.log("isHovering", isHovering)

  const Action = ({children}: { children: ReactNode}) => (
    <div>
      {children}
    </div>
  );

  return (
    <div >
      <Action>
        <Counter />
      </Action>
    </div>
  );
};
export default Index;

示例代码没有复现哦~

https://github.com/alibaba/hooks/assets/38221479/83f42b4c-8592-4e0d-8cdf-358fba2f0129

github-actions[bot] commented 1 year ago

Hi, lifegit.

It seems that this issue is a bit vague and lacks some necessary information.

看起来这条 issue 描述得有些模糊,缺少一些必要的信息。

lifegit commented 1 year ago

不好意思。在示例中有个错误,忘了将ref挂载到div中,现已修改过来。 复现 repo:https://github.com/lifegit/test-ahhoks-userhover

liuyib commented 1 year ago

这是正常逻辑,hover 完不 rerender 你的组件怎么获取到新的状态呢? @lifegit

liuyib commented 1 year ago

@lifegit

useHover 是没问题的,问题在于你的使用问题。

一句话解释就是:父组件重渲染引起嵌套的子组件全新渲染了。

详细点解释就是:useHover 引起 Index 重渲染,导致 Action 的引用变了,Action 变成了全新的组件又导致 Action 和 Counter 都全新渲染。

这就是为什么很多人不推荐写嵌套的函数组件,因为有坑(当你知道坑以及解决办法时,还是可以用一用的)。

解决办法:

  1. 如你所说,把 Action 提到外面,可以防止 Index 组件重渲染时,Action 组件的引用变动
  2. useMemo 包一下 Action 组件,使其引用维持不变。
  3. ref 放在 Counter 组件里:
import { useHover } from 'ahooks';
import React, { useRef, useState } from 'react';
import type { ReactNode } from 'react';
import { Button } from 'antd';

const Counter = () => {
  // 注意 ref 放在了这里
  const ref = useRef<HTMLDivElement>(null);
  const isHovering = useHover(ref);
  console.log('isHovering', isHovering);
  const [state, setState] = useState(0);

  return (
    <div>
      <Button ref={ref} onClick={() => setState((v) => v + 1)}>
        + {state}
      </Button>
    </div>
  );
};

const Index = () => {
  const Action = ({ children }: { children: ReactNode }) => <div>{children}</div>;

  return (
    <div>
      <Action>
        <Counter />
      </Action>
    </div>
  );
};

export default Index;