React Hooks 带来的困扰与思考

当前使用 React 版本:v16.10.2react

自从 React Hooks 推出以来,给平常工做带来新的开发体验,提升开发效率的同时也增长了代码的可阅读性。本人最近恰好接到一个从零开始的项目需求,趁这个机会就采用 Hooks 函数式组件进行开发,目前代码量已有1w+。相较于以前的 class 组件,Hooks 下的函数式组件确实带来了很多便利性,让我这个从 Vue 转过来的从新喜欢上了 React。好处之类的就不详细说了,各大文章也有介绍,本文主要罗列一下 Hooks 在平常开发中遇到一些纠结的地方。git

依赖数组的正确性问题

在官方文档中,一直强调要确保 useEffect 依赖数组的正确性,反作用的函数中,使用了 props 或 state 变量必定要存在于依赖数组中。使用了官方提供的 eslint 配置后,反作用函数中有 props 或 state 变量未存在于依赖数组中,会有 exhaustive-deps 规则的提醒。然而在某些场景下,这又与这个规则不符。github

好比:但愿 useEffect 只关心部分 props 或 state 变量的变更,从而从新执行反作用函数,其它 props 或 state 变量只取决于当时的状态。npm

场景1:只在组件 mount 以后执行的方法。数组

例子中但愿实现 componentDidMount 相似的功能,可是由于引用了 count 变量且未在 useEffect 的依赖数组中声明,就触发了 exhaustive-deps 规则的提醒。出于对 eslint 规则的遵照(强迫症),在不由用该规则的前提下,只好经过 useRef 来避开这个规则。对反作用函数中每一个 props 或 state 变量都建立对应的 useRef 值显然比较麻烦且不合理,那就拿反作用函数来操做好了。为了便于复用,封装成 useMount 函数,也可以使用 react-use 库。bash

function useMount(mountedFn) {
  const mountedFnRef = useRef(null);

  mountedFnRef.current = mountedFn;

  useEffect(() => {
    mountedFnRef.current();
  }, [mountedFnRef]);
}
复制代码

使用后,warning 解除。闭包

场景2:只关心部分变量的变更。函数

例子中的 Modal 组件须要根据 visible 变量的变更来执行相应的方法,又须要引用到其它的 props 或 state 变量,可是又不但愿将它们放入 useEffect 依赖数组里,由于不关心它们的变更。若是将它们放入 useEffect 数组中,在 visible 变量不变的状况下,其它变量的变更会带来反作用函数的重复执行,这多是非预期的。这时就须要一个辅助变量来记录 visible 变量的前一状态值,用来在反作用函数中判断是否由于 visible 变量变更触发的函数执行。为了便于复用,封装成 usePrevious 函数,也可以使用 react-use 库。ui

const usePrevious = (value) => {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  });

  return ref.current;
};
复制代码

使用后,warning 解除。this

从上面的例子能够看出,咱们但愿 useEffect 的依赖数组中是与反作用函数更有效的变量,而不是反作用函数中所有引用的变量。而从官方提供的文档和 eslint 规则来看,这彷佛与官方传达的意图不符。若是要遵循官方更为严格的规则,就须要写更多的条件判断。

依赖数组中变量的比较问题

React 对于各 hook 函数依赖数组中变量的比较都是严格比较(===),因此咱们须要注意对象类型变量的引用。相似下面的状况应该尽可能避免,由于每次 App 组件渲染,传给 Child 组件的 list 变量都是一个全新引用地址的数组。若是 Child 组件将 list 变量放入了某个 hook 函数的依赖数组里,就会引发该 hook 函数的依赖变更。

function App() {
  const list = [1, 2, 3];

  return (
    <>
      <Child list={list}></Child>
      <Child list={[4, 5, 6]}></Child>
    </>
  );
}
复制代码

上面这种状况多加注意仍是能够避免的,但在某些状况下咱们但愿依赖数组中对象类型的比较是浅比较或深比较。在 componnetDidUpdate 声明周期函数中这确实不难实现,但在函数式组件中仍是须要借助 useRef 函数。

例子:

import { isEqual } from 'lodash';

function useCampare(value, compare) {
  const ref = useRef(null);

  if (!compare(value, ref.current)) {
    ref.current = value;
  }

  return ref.current;
}

function Child({ list }) {
  const listArr = useCampare(list, isEqual);

  useEffect(() => {
    console.log(listArr);
  }, [listArr]);
}
复制代码

在该例子中,使用了一个 ref 变量,每次组件渲染时都会取以前的值与当前值进行自定义函数的比较,若是不相同,则覆盖当前值,最后返回 ref.current 值。从而实现了自定义依赖数组比较方法的功能。

函数引用变更问题

在 class 组件中,传递给子组件的 props 变量中函数大多数都是 this.xxx 形式,即引用都是稳定的。为了让函数能保持引用稳定,React 提供了 useCallback 函数,只要依赖数组保持稳定,会返回一个引用稳定的函数。若是要严格遵循该要求,useCallback 可能会成整个项目中最经常使用的 API。但当咱们耗费心思去保持函数的引用稳定,可是组件树上层一个不当心可能就将以前的努力白费。

好比:

function Button({
  child,
  disabled,
  onClick
}) {
  /**
   * 备注:
   * 如何不将 disabled 和 onClick  变量加入依赖数组中,
   * handleBtnClick 函数触发时,只会取得第一次渲染的值。
   */
  const handleBtnClick = useCallback(() => {
    if (!disabled && onClick) {
      onClick();
    }
  }, [disabled, onClick]);

  return (
    <button onClick={handleBtnClick}>{child}</button>
  );
}

function App() {
  const onBtnClick = () => {};

  return (
    <Button onClick={onBtnClick} />
  );
}
复制代码

上面例子中,咱们但愿 Button 组件中 handleBtnClick 函数只在 disabled 和 onClick 变量的引用变更时才返回一个新的引用的函数。但在 App 组件中却没有使用 useCallback 来保持 onBtnClick 函数的引用稳定,从而每次渲染传递给 Button 组件 都是全新引用的函数。子组件接收到这个全新引用的函数,就会触发 useCallback 的依赖变更从新生成一个全新引用的 handleBtnClick 函数。

并且在列表渲染中,咱们但愿在传递给子组件的函数变量中增长值,必然会致使每次父组件渲染,传递给子组件的都是全新引用的函数。

function App() {
  const [list] = useState(() => {
    return [1, 2, 3];
  });
  const handleItemClick = useCallback((item) => {
    console.log(item);
  }, []);

  return (
    <div>
      {
        list.map((value) => (
          <Item
            key={value}
            onClick={() => handleItemClick(value)}
          />
        ))
      }
    </div>
  );
}
复制代码

这样,咱们在子组件使用 useCallback 反而带来了没必要要的比较。其实,对于组件 props 中函数的变量还可使用如下这种形式。

function Button({
  child,
  disabled,
  onClick
}) {
  const handleBtnClickRef = useRef();

  handleBtnClickRef.current = () => {
    if (!disabled && onClick) {
      onClick();
    }
  };

  const handleBtnClick = useCallback(() => {
    handleBtnClickRef.current();
  }, [handleBtnClickRef]);

  return (
    <button onClick={handleBtnClick}>{child}</button>
  );
}
复制代码

上面例子中,使用了一个 useRef 函数返回的变量 handleBtnClickRef 来保存最新的函数。由于该变量引用是固定的,因此 handleBtnClick 函数的引用也是固定的,触发 onClick 回调函数也能拿到最新的 disabled 和 onClick 值。

那么在平常开发中,到底是使用 useCallback 方案,仍是使用 useCallback + useRef 的 hack 方案呢?这里还想听听各位意见。

useRef 的使用

在整篇文章中,useRef 成了各类问题的解决方案,这种 mutable 的方式实现了相似于 class 的 this 功能。这种方式能够很好的解决闭包带来的不方便性,可是有时仍是会纠结该不应都用这种方式。

好比 useEffect 中进行全局事件的绑定:

function App() {
  const handleResize = useCallback(() => {
    console.log(count);  
  }, [count]);
  useEffect(() => {
    window.addEventListener('resize', handleResize);
    
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, [handleResize]);
}
复制代码

在例子中,每次 count 变量的变更都会引发 handleResize 变量的变更,进而引发下方 useEffect 中反作用函数的执行,即执行事件解绑 + 事件绑定。而实际上,咱们只但愿在组件挂载时绑定 resize 事件,在组件销毁时解绑。若是要实现这样的功能,又须要借助 useRef。

function App() {
  const handleResizeRef = useRef();
  
  handleResizeRef.current = () => {
    console.log(count);  
  };

  useEffect(() => {
    const handleResize = () => {
        handleResizeRef.current();
    };
    window.addEventListener('resize', handleResize);
    
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, [handleResize]);
}
复制代码

目前,我不太清楚本身是否太在乎事件的绑定与解绑的次数,又或者说仍用 class 组件生命周期函数来对待 useEffect。固然,从 useEffect 定义来看,是声明依赖于一系列数据的反作用,这些数据的变更必然须要致使反作用的变更。可是这种声明式反作用在一些场景下会带来多余的代码执行,好比上面例子中重复的解绑与绑定。

这些都是最近项目实践中遇到的一些问题点,在此仅作记录,留待之后想通。

相关文章
相关标签/搜索