React Hooks初步踩坑坑

3,089 阅读3分钟

大家好,我是踩坑小星球。作为一个 Component 拥护者,迈出一小步踩下 Hook ,看看它到底是个啥玩意儿。 基础语法之类的就不说了,文章多的是,这个只是我自己在开发的时候,需要注意的点~ [TOC]

关于状态

  1. useState管理状态,这里有一点需要注意,当你使用 useState 的 set方法的时候,旧状态不会自动 merge到新状态中去,也就是 set 所改变的是全量数据。
    这句话的意思就是,如果你的 state 是一个对象,你需要手动的构成一个完整的新数据
  // 定义
  const [obj,setObj] = useState({name: "小蘑菇",age: 0 });
  
  // 更新
  setObj({
    ...obj,
    name: "大蘑菇"
  });

  // 结果
  // {name: "大蘑菇",age: 0 }
  1. useState产生的 state 作为函数中的一个常量,就是普通的数据,并不存在诸如数据绑定这样的操作来驱使 DOM 发生更新。在调用 setState 后,React 将重新执行 render 函数,仅此而已。因此,状态也是函数作用域下的普通变量。
    我们可以说每次函数执行拥有独立的状态,具有 Capture Value 的特性,简单来说,就是它不能实时。

  2. 利用useRef就可以绕过 Capture Value 的特性。可以认为 ref 在所有 Render 过程中保持着唯一引用,因此所有对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render 间存在隔离。
    原因是因为,ref.current 将被赋予初始值 initialValue,之后便不再发生变化。但你可以自己去设置它的值。设置它的值不会重新触发 render 函数。
    因此useRef除了用于获取dom节点外,还有一个强大的作用就是缓存数据,因为它不会触发函数更新;react-redux源码中的connectAdvanced等模块就用了这一技能

function Example() {
  const [count, setCount] = useState(0);
  // Initial ref
  const latestCount = useRef(count);

  useEffect(() => {
    // Set the mutable latest value
    latestCount.current = count;
    setTimeout(() => {
      // Read the mutable latest value
      console.log(`You clicked ${latestCount.current} times`);
    }, 3000);
  });
  // ...
}

生命周期

Component 最大的好处是有生命周期可以处理各种数据,在 Hook 也可以利用Effect钩子模拟这种生命周期

  1. 通常我们会利用组件的生命周期函数去执行一些副作用,但通常副作用逻辑复杂,可能会有 Bug,所以在 Hook 里,建议针对每一段逻辑单独使用一个Effect钩子
  useEffect(() => {
      console.log('模拟componentDidMount');
      return () => {
      	console.log('模拟componentWillUnmount')
      }
  }, []);
  
  useEffect(() => {
      console.log('模拟componentDidMount以及name改变时,模拟componentDidUpdate');
      return () => {
      	console.log('name改变时,都会先触发这里,再触发上面的effect')
      }
  }, [name]);

  1. 对于 useEffect 来说,执行的时机是完成所有的 DOM 变更并让浏览器渲染页面后,而 useLayoutEffect 和 class 组件中 componentDidMount, componentDidUpdate一致——在 React 完成 DOM 更新后马上同步调用,会阻塞页面渲染.

  2. 利用useMemo减少不必要的渲染

function Parent({ a, b }) {
  // Only re-rendered if `a` changes:
  const child1 = useMemo(() => <Child1 a={a} />, [a]);
  // Only re-rendered if `b` changes:
  const child2 = useMemo(() => <Child2 b={b} />, [b]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}

特殊场景

  1. 依赖了某些值,但是不要在初始化就执行回调,要在依赖改变再去执行回调,模拟componentDidUpdate
const init = useRef(true);
useEffect(() => {
  if (init.current) {
    init.current = false;
    return;
  }
  // do something...
}, [ xxx ]);

  1. 循环/判断中使用Hook

不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们 官方文档是这么说的。实际情况是,由于Hook是基于链表进行注册的,有一个固定的顺序,如果使用判断或循环,会导致执行顺序不一样。但我的理解\color{#FF7F50}{我的理解}是,只要能保证执行顺序固定,是可以在条件或循环中使用Hook的。

  useEffect(() => {
    func1();
    func2(a);
  }, [a]);
  
  useEffect(() => {
    func1();
    func2(b);
  }, [b]);
  
  useEffect(() => {
    func1();
    func2(c);
  }, [c]);

上述例子,可以发现,重复代码过多,可以使用循环来执行userEffect

  const arr = [a,b,c];
  for (let i of arr){
    useEffect(() => {
      func1();
      func2(i);
    }, [i]);
  }
  1. userEffect中使用异步函数

useEffect是不能直接用 async await 语法糖的 若想使用,写成立即执行函数或useEffect中创建异步函数后再执行

  useEffect(() => {
    // Using an IIFE
    (async function asyncFunction() {
      await loadContent();
    })();
  },[]);
useEffect(() => {
  const asyncFunction = async () =>{
      const data = await loadContent();
      setData(data)
  }
  asyncFunction();
}, []);

注意事项

  • useLayoutEffect可能会阻塞浏览器的绘制
    useEffect执行顺序: 组件更新挂载完成 -> 浏览器dom绘制完成 -> 执行 useEffect 回调
    useLayoutEffect执行顺序: 组件更新挂载完成 -> 执行 useLayoutEffect 回调-> 浏览器dom绘制完成
  • 不要在循环,条件或嵌套函数中调用 Hook ,必须始终在 React 函数的顶层使用 Hook 。这是因为React需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。
  • 只能在 React 函数式组件或自定义 Hook 中使用 Hook
  • 可以安装 eslint 插件eslint-plugin-react-hooks,在.eslintrc.js中加入配置
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error"
  }
}
  • 谨慎将处于同个 useEffect dependences 之中且有逻辑关联的 state 放在多个 useEffect中
  • 使用useMemo这种会缓存的API时,需要将其依赖的state值放到第二个参数里,否则可能获取不到最新的state