写React Hooks前必读

最近团队内有同窗,因为写react hooks引起了一些bug,甚至有1例是线上问题。团队内也所以发起了一些争执,到底要不要写hooks?到底要不要加lint?到底要不要加autofix?争论下来结论以下:javascript

  1. 写仍是要写的;
  2. 写hooks前必定要先学习hooks;
  3. 团队再出一篇必读文档,必需要求每位同窗,先读再写。

所以便有了此文。html

本文主要讲两大点:前端

  1. 写hooks前的硬性要求;
  2. 写hooks常见的几个注意点。

硬性要求

1. 必须完整阅读一次React Hooks官方文档

英文文档:reactjs.org/docs/hooks-…
中文文档:zh-hans.reactjs.org/docs/hooks-…
其中重点必看hooks: useState、useReducer、useEffect、useCallback、useMemojava

另外推荐阅读:react

  1. Dan的《useEffect彻底指南》
  2. 衍良同窗的《React Hooks彻底上手指南

2. 工程必须引入lint插件,并开启相应规则

lint插件:www.npmjs.com/package/esl…
必开规则:npm

{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}
复制代码

其中, react-hooks/exhaustive-deps 至少warn,也能够是error。建议全新的工程直接配"error",历史工程配"warn"。json

切记,本条是硬性条件。api

若是你的工程,当前没开启hooks lint rule,请不要编写任何hooks代码。若是你CR代码时,发现对方前端工程,没有开启相应规则,而且提交了hooks代码,请不要合并。该要求适应于任何一个React前端工程。数组

这两条规则会避免咱们踩坑。虽然对于hooks新手,这个过程可能会比较“痛苦”。不过,若是你以为这两个规则对你编写代码形成了困扰,那说明你还未彻底掌握hooks。缓存

若是对于某些场景,确实不须要「exhaustive-deps」,可在代码处加:
// eslint-disable-next-line react-hooks/exhaustive-deps

切记只能禁本处代码,不能偷懒把整个文件都禁了。

3. 如如有发现hooks相关lint致使的warning,不要全局autofix

除了hooks外,正常的lint基本不会改变代码逻辑,只是调整编写规范。可是hooks的lint规则不一样,exhaustive-deps 的变化会致使代码逻辑发生变化,这极容易引起线上问题,因此对于hooks的waning,请不要作全局autofix操做。除非保证每处逻辑都作到了充分回归。

另外公司内部有个小姐姐补充道:eslint-plugin-react-hooks 从2.4.0版本开始,已经取消了 exhaustive-deps 的autofix。因此,请尽可能升级工程的lint插件至最新版,减小出错风险

而后建议开启vscode的「autofix on save」。将来不管是什么问题,能把error与warning 尽可能遏制在最开始的开发阶段,保证自测跟测试时就是符合规则的代码。

常见注意点

依赖问题

依赖与闭包问题是必定要开启exhaustive-deps 的核心缘由。最多见的错误即:mount时绑定事件,后续状态更新出错。

错误代码示例:(此处用addEventListener作onclick绑定,只是为了方便说明状况)

function ErrorDemo() {
  const [count, setCount] = useState(0);
  const dom = useRef(null);
  useEffect(() => {
    dom.current.addEventListener('click', () => setCount(count + 1));
  }, []);
  return <div ref={dom}>{count}</div>;
}
复制代码

这段代码的初始想法是:每当用户点击dom,count就加1。理想中的效果是一直点,一直加。但实际效果是 {count} 到「1」之后就加不上了。

咱们来梳理一下, useEffect(fn, [])  表明只会在mount时触发。也便是首次render时,fn执行一次,绑定了点击事件,点击触发 setCount(count + 1) 。乍一想,count仍是那个count,确定会一直加上去呀,固然现实在啪啪打脸。

状态变动 触发 页面渲染的本质是什么?本质就是 ui = fn(props, state, context) 。props、内部状态、上下文的变动,都会致使渲染函数(此处就是ErrorDemo)的从新执行,而后返回新的view。

那如今问题来了, ErrorDemo 这个函数执行了屡次,第一次函数内部的 count 跟后面几回的 count 会有关系吗?这么一想,感受又应该没有关系了。那为何 第二次又知道 count 是1,而不是0了呢?第一次的 setCount 跟后面的是同一个函数吗?这背后涉及到hooks的一些底层原理,也关系到了为何hooks的声明须要声明在函数顶部,不容许在条件语句中声明。在这里就很少讲了。

结论是:每次 count 都是从新声明的变量,指向一个全新的数据;每次的 setCount 虽然是从新声明的,但指向的是同一个引用。

回到正题,咱们知道了每次render,内部的count其实都是全新的一个变量。那咱们绑定的点击事件方法,也即:setCount(count + 1) ,这里的count,其实指的一直是首次render时的那个count,因此一直是0 ,所以 setCount,一直是设置count为1。

那这个问题怎么解?

首先,应该遵照前面的硬性要求,必需要加lint规则,并开启autofix on save。而后就会发现,其实这个 effect 是依赖 count 的。autofix 会帮你自动补上依赖,代码变成这样:

useEffect(() => {
  dom.current.addEventListener('click', () => setCount(count + 1));
}, [count]);
复制代码

那这样确定就不对了,至关于每次count变化,都会从新绑定一次事件。因此对于事件的绑定,或者相似的场景,有几种思路,我按个人常规处理优先级排列:

思路1:消除依赖

在这个场景里,很简单,咱们主要利用 setCount 的另外一个用法 functional updates。这样写就行了:
() => setCount(prevCount => ++prevCount) ,不用关心什么新的旧的、什么闭包,省心省事。

思路2:从新绑定事件

那若是咱们这个事件就是要消费这个count怎么办?好比这样:

dom.current.addEventListener('click', () => {
  console.log(count);
  setCount(prevCount => ++prevCount);
});
复制代码

咱们没必要执着于必定只在mount时执行一次。也能够每次从新render前移除事件,render后绑定事件便可。这里利用useEffect的特性,具体能够本身看文档:

useEffect(() => {
  const $dom = dom.current;
  const event = () => {
    console.log(count);
    setCount(prev => ++prev);
  };
  $dom.addEventListener('click', event);
  return () => $dom.removeEventListener('click', event);
}, [count]);
复制代码

**思路3:若是嫌这样开销大,或者编写麻烦,也能够用 useRef **

其实用 useRef 也挺麻烦的,我我的不太喜欢这样操做,但也能解决问题,代码以下:

const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
  dom.current.addEventListener('click', () => {
    console.log(countRef.current);
    setCount(prevCount => {
      const newCount = ++prevCount;
      countRef.current = newCount;
      return newCount;
    });
  });
}, []);
复制代码

useCallback与useMemo

这两个api,其实概念上仍是很好理解的,一个是「缓存函数」, 一个是缓存「函数的返回值」。但咱们常常会懒得用,甚至有的时候会用错。

从上面依赖问题咱们其实能够知道,hooks对「有没有变化」这个点其实很敏感。若是一个effect内部使用了某数据或者方法。若咱们依赖项不加上它,那很容易因为闭包问题,致使数据或方法,都不是咱们理想中的那个它。若是咱们加上它,极可能又会因为他们的变更,致使effect疯狂的执行。真实开发的话,你们应该会常常遇到这种问题。

因此,在此建议:

  1. 在组件内部,那些会成为其余useEffect依赖项的方法,建议用 useCallback 包裹,或者直接编写在引用它的useEffect中。
  2. 己所不欲勿施于人,若是你的function会做为props传递给子组件,请必定要使用 useCallback 包裹,对于子组件来讲,若是每次render都会致使你传递的函数发生变化,可能会对它形成很是大的困扰。同时也不利于react作渲染优化。

不过还有一种场景,你们很容易忽视,并且还很容易将useCallback与useMemo混淆,典型场景就是:节流防抖。

举个例子:

function BadDemo() {
  const [count, setCount] = useState(1);
  const handleClick = debounce(() => {
    setCount(c => ++c);
  }, 2000);
  return <div onClick={handleClick}>{count}</div>;
}
复制代码

咱们但愿每2秒只能触发一次 count + 1 ,这个组件在理想逻辑下是OK的。但现实是骨感的,咱们的页面组件很是多,这个 BadDemo 可能因为父级什么操做就从新render了。如今假使咱们页面每500毫秒会从新render一次,那么就是这样:

function BadDemo() {
  const [count, setCount] = useState(1);
  const [, setRerender] = useState(false);
  const handleClick = debounce(() => {
    setCount(c => ++c);
  }, 2000);
  useEffect(() => {
    // 每500ms,组件从新render
    window.setInterval(() => {
      setRerender(r => !r);
    }, 500);
  }, []);
  return <div onClick={handleClick}>{count}</div>;
}
复制代码

每次render致使handleClick实际上是不一样的函数,那么这个防抖天然而然就失效了。这样的状况对于一些防重点要求特别高的场景,是有着较大的线上风险的。

那怎么办呢?天然是想加上 useCallback :

const handleClick = useCallback(debounce(() => {
  setCount(c => ++c);
}, 2000), []);
复制代码

如今咱们发现效果知足咱们指望了,但这背后还藏着一个惊天大坑。
假如说,这个防抖的函数有一些依赖呢?好比 setCount(c => ++c); 变成了 setCount(count + 1) 。那这个函数就依赖了 count 。代码就变成了这样:

const handleClick = useCallback(
  debounce(() => {
    setCount(count + 1);
  }, 1000),
  []
);
复制代码

你们会发现,你的lint规则,居然不会要求你把 count 做为依赖项,填充到deps数组中去。这进而致使了最初的那个问题,只有第一次点击会count++。这是为何呢?

由于传入useCallback的是一段执行语句,而不是一个函数声明。只是说它执行之后返回的新函数,咱们将其做为了 useCallback 函数的入参,而这个新函数具体是个啥,其实lint规则也不知道。

正确的姿式应该是使用 useMemo :

const handleClick = useMemo(
  () => debounce(() => {
    setCount(count + 1);
  }, 1000),
  [count]
);
复制代码

这样保证每当 count 发生变化时,会返回一个新的加了防抖功能的新函数。

总而言之,对于使用高阶函数的场景,建议一概使用 useMemo 。

其余的注意点,后面想到了再持续补充,或者欢迎回复~

相关文章
相关标签/搜索