shaozj / blog

blog
87 stars 4 forks source link

hooks 陷阱 #37

Open shaozj opened 4 years ago

shaozj commented 4 years ago

本文从实际示例出发,分析 hooks 在使用时容易遇到的陷阱,以及提出如何避免掉进 hooks 陷阱的方法。

问题一

下方的代码存在什么问题?

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

function App() {
  const [data, setData] = useState();
  useEffect(async () => {
    const result = await fetch(
      'https://hn.algolia.com/api/v1/search?query=redux',
    );
    setData(result.data);
  }, []);
  return (
    <div>
      {data}
    </div>
  );
}
export default App;


答:会出现 warning,useEffect 应该返回一个清理函数或者什么都不返回。 Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => ...) are not supported, but you can call an async function inside an effect.

正确写法:

useEffect(() => {
  const fetchData = async () => {
    const result = await fetch(
      'https://hn.algolia.com/api/v1/search?query=redux',
    );
    setData(result.data);
  };
  fetchData();
}, []);


问题二

下方强制更新组件的代码能 work 吗?

const setUpdate = useState()[1];
const forceUpdate = () => setUpdate();

forceUpdate();


答:不能 work,在函数式组件中,如果 setState 没有修改对应 state 的值(浅比较相等),那么不会触发 re-render,这点和 class 组件不同,class 组件只要 setState 就会触发 re-render,因为它每次会合并 state 中所有属性,生成一个新的 state。

正确写法:

const setUpdate = useState()[1];
const forceUpdate = () => setUpdate({});

forceUpdate();

问题三

下方代码存在什么问题?

function Timer() {
    const [count, setCount] = React.useState(0);

    useEffect(() => {
        const intervalId = setInterval(() => {
            setCount(count + 1);
        }, 1000);
        return () => clearInterval(intervalId);
    }, []);

    return (
        <div>The count is: {count}</div>
    );
}


答:这是一个最常见的说明 useEffect 陷阱的例子了。在上方的代码运行后,count 首先为 0,一秒后更新为 1,之后就不变化了。这是由闭包导致的,useEffect 中的 count 值始终为 0,setInterval 每次执行都将其更新为 1。

方案1:依赖数组中每个依赖的外部变量都添加进去(性能不佳)

function Timer() {
    const [count, setCount] = React.useState(0);

    useEffect(() => {
        const intervalId = setInterval(() => {
            setCount(count + 1);
        }, 1000);
        return () => clearInterval(intervalId);
    }, [count]);

    return (
        <div>The count is: {count}</div>
    );
}


方案2:使用 useRef 生成一个可变对象,记住我们的变量(个人最推荐,适用于所有需要依赖外部变量的情况)

function Timer() {
    const [count, setCount] = React.useState(0);
    const countRef = React.useRef(0);

    useEffect(() => {
        const intervalId = setInterval(() => {
            countRef.current = countRef.current + 1;
            setCount(countRef.current);
        }, 1000);
        return () => clearInterval(intervalId);
    }, []);

    return (
        <div>The count is: {count}</div>
    );
}



方案3:如果是依赖外部变量做 setState,可以用 functional-updates 然后把依赖的变量去除(很适合这个例子,但是适用范围有限)

function Timer() {
    const [count, setCount] = React.useState(0);

    useEffect(() => {
        const intervalId = setInterval(() => {
            setCount(count => count + 1);
        }, 1000);
        return () => clearInterval(intervalId);
    }, []);

    return (
        <div>The count is: {count}</div>
    );
}

问题四

我想使用类似于 component 组件中的 this 该怎么办,用它跟踪变量的变化,但是不触发 re-render.

答:这个问题前面其实已经给出答案了,那就是使用 useRef。

问题五

以下代码存在什么问题?该如何修改?

let stopPolling = false;

function pay({ visible }) {
  const [data, setData] = useState();

    function polling() {
    if (stopPolling) {
      return;
    }
    fetch().then(
        if (res.finished) {
            setData(res.data);
        } else {
        setTimeout(polling, 1000); 
      }
    );
  }

  useEffect(() => {
    stopPolling = !visible;
  }, [visible]);

  return <div onClick={polling}>{data}</div>;
}

最后这个问题留作课后作业吧