likes
comments
collection

好吧,useEffect会用就行

作者站长头像
站长
· 阅读数 19

前言

之前也看过关于useEffect的文章,但是真到自己使用,或者叫我讲出为啥的时候,我是真讲不出来。但是有些项目却总是开启了useEffect依赖检测,如果依赖没有使用正确的话控制台会warning或者直接报错。没办法,虽然我记不住啥啥啥原理,我知道怎么用就好了。虽然说是这么说,但是基础的东西是要知道的

前提

请开启useEffect依赖规则,如果不开启配置的话,我们有时候甚至不会去关注useEffect依赖问题。 yarn add eslint-plugin-react-hooks --dev

然后在.eslintrc.json文件中,引入规则。

  1. 在plugin: ["react-hooks"]

  2. 在rules: { "react-hooks/rules-of-hooks": "warn", // 检查 Hook 的规则 "react-hooks/exhaustive-deps": "warn", // 检查 effect 的依赖 } 如何使用webpack配置react项目可以看我之前写的文章。上面的配置也是写在之前写文章时用的项目中。

useEffect是用来干什么的?

useEffect本身就是一个函数,接受一个函数作为参数,然后这个函数会在组件渲染到屏幕之后执行。由于是每轮渲染完后执行,所以它可以做一下改变DOM、添加订阅、设置定时器,请求接口数据等有副作用的操作。因为useEffect是放到组件里面,所以组件的每次更新理论上来讲都会生成一个新的useEffect并且执行,但是我们可以有选择性的让他执行,这个就是我们需要讲的依赖。反正我用来请求接口居多...

没有依赖

不需要依赖就是useEffect中传入一个函数作为第一个参数,第二个参数不传。这样每次组件状态更新useEffect都会更新(更新前会记录当前的状态)并且执行,意思就是不会被当前状态影响。如下

const [count, setCount] = useState(0);
   useEffect(() => {
       console.log('count', count);
   })

如果没有依赖,每次count改变,都会打印最新的count。

前面一直说,每次更新都会一个新的useEffect,并且更新前会记录当前的状态。例子如下

const Dashboard = () => {
   const [count, setCount] = useState(0);
   const handleClick = () => {
       setCount(count + 1)
   }

   useEffect(() => {
       setTimeout(() => {
           console.log('count', count);
       }, 2000)
   })
   return (
       <>
           ---{count}---
           <button onClick={handleClick}>点击+1</button>
       </>
   );
}

如果我连续点击3次button是count加1,大家可以思考下,setTimeout里面输出的是什么样子的?是0,1,2,3?还是全都是3. 答案是前者,因为组件中的函数(包括useEffect)会捕获每次状态更新时的状态。

有依赖

依赖以数组的方式作为useEffect的第二个参数。当依赖发生改变useEffect会再次执行。下面有几个例子是当使用依赖报错时的解决办法

场景一: []作为依赖

[]作为依赖,原意是为了让页面一挂载就执行,并且只执行一次。但有时候这样往往不尽人意。

    useEffect(() => {
        console.log('页面挂载我就渲染,组件状态改变我是不会渲染的');
    }, []);

场景二:页面一挂载就开启倒计时

强行举个场景例子。例如一进入这个页面,我就开启一个倒计时(我只想运行一次useEffect),倒计时过后退出这个页面。你可能会这样写

import React, {
  useEffect,
  useState
} from 'react';
const Dashboard = () => {
  const [count, setCount] = useState(60);

  useEffect(() => {
      const timer = setInterval(() => {
          setCount(count -1)
      }, 1000)
      return () => clearInterval(timer)
  }, [])
  return (
      <>
          ---{count}---
      </>
  );
}

export default Dashboard

这样写,如果开起来依赖报错,那么编辑器肯定就会提醒你依赖错误,如

好吧,useEffect会用就行

抛开这个报错,你认为count会如何变化。运行下代码就知道了,count变为59就不会变了。因为由于useEffect依赖设置为[],所以只会执行一次,然后加上useEffect会记录之前的状态,所以记录的是count为60的状态。于是每隔一秒钟setInterval执行的时候count都是60,所以页面会一直显示59 如果你根据提示添加count作为依赖。首先效果可以达到,但不是很理想。分析一下,如果count作为依赖,第一次count为60,然后useEffect开始执行,每隔一秒钟count减1,即count一直再变。那么根据依赖count在变,useEffect会重新执行,每次新建一个定时器,然后再次渲染之后又会清除前面一个定时器再新建一个。虽然能实现效果,但是不完美。

比较正确的做法就是尽量不依靠依赖,尽量不要在useEffect中使用组件里面的状态。比如说这个count我们其实可以不依赖这个count,我们可以这样做

useEffect(() => {
       const timer = setInterval(() => {
           setCount(prev => prev - 1)
       }, 1000)
       return () => clearInterval(timer)
   }, [])

场景三:函数作为依赖

函数作为依赖第一印象是不行的,因为组件每次更新同一个函数都会重新生成,并且与之前都不一样,所以如果单纯的把函数作为依赖,那么useEffect会无限调用。

有这样的场景,页面一挂载完成就要去请求数据.

  1. 单独写一个方法,然后放到useEffect中,这个方面不适用任何一个组件中的状态如下处理,也可以把fetchData方法提出来,放到组件外面,这样也可以减少每次组件渲染重复加载这个方法。
import React, {
    useEffect,
    useState
} from 'react';
import Axios from 'axios';
const Dashboard = () => {
    const [userId, setUserId] = useState(101);
    const [data, setData] = useState<any[]>([]);
    const fetchData = async () => {
        const result = await Axios({
            headers: {
                'Content-type': 'application/json; charset=UTF-8',
            },
            method: 'post',
            url: 'https://jsonplaceholder.typicode.com/posts',
            data: JSON.stringify({
                title: 'foo',
                body: 'bar',
                userId: 1
            })
        }).then((res: any) => {
            console.log('res', res);
            return []
        });
        setData(result)
    }

    useEffect(() => {
        fetchData()
    }, []);
    return (
        <>
            ---{count}---
        </>
    );
}

export default Dashboard
  1. 单独写一个方法,但是需要用到组件中的状态

如果你直接把userId,如下直接放入fetchData方法中,那么编辑器会报错,会提醒你需要fetchData这个方法作为依赖。好,然后我们就继续把fetchData 这个方法作为useEffect的依赖好吧,useEffect会用就行

好吧,useEffect会用就行 第一步

import React, {
    useEffect,
    useState
} from 'react';
import Axios from 'axios';
const Dashboard = () => {
    const [userId, setUserId] = useState(101);
    const [data, setData] = useState<any[]>([]);
    const fetchData = async () => {
        const result = await Axios({
            headers: {
                'Content-type': 'application/json; charset=UTF-8',
            },
            method: 'post',
            url: 'https://jsonplaceholder.typicode.com/posts',
            data: JSON.stringify({
                title: 'foo',
                body: 'bar',
                userId: userId
            })
        }).then((res: any) => {
            console.log('res', res);
            return []
        });
        setData(result)
    }

    useEffect(() => {
        fetchData()
    }, []);
    return (
        <>
            ---{userId}---
        </>
    );
}

第二步 把fetchData作为依赖,这就比较坑了。如果你没有开启依赖报错,那么页面已刷新会一直执行featData这个方法。分析下,因为fetchData在每次组件更新时,都会生成新的fetchData与之前fetchData不一样,所以useEffect就会不断执行。怎么解决这个问题嘞,我们可以使用useCallback(因为useCallback第二个参数也可以传递依赖,当这个依赖发生变化时,fetchData才会重新执行)

好吧,useEffect会用就行

useEffect(() => {
  fetchData()
}, [fetchData]);

第三步 用useCallback去包裹fetchaData。如下,这样就可以了,只有userId发生改变的时候useEffect才会重新执行

const fetchData = useCallback(async () => {
        const result = await Axios({
            headers: {
                'Content-type': 'application/json; charset=UTF-8',
            },
            method: 'post',
            url: 'https://jsonplaceholder.typicode.com/posts',
            data: JSON.stringify({
                title: 'foo',
                body: 'bar',
                userId: userId
            })
            // data: {a: count}
        }).then((res: any) => {
            console.log('res', res);
            return []
        });
        setData(result)
    }, [userId])

函数做依赖总结

  1. 如果这个方法不需要状态(state),那么可以把这个方法单独提出这个组件。
  2. 如果需要state且这个方法不需要被复用,可以把这个方法放到useEffect中,然后把这个state作为useEffect的依赖.
  useEffect(() => {
      const fetchData = async () => {
          const result = await Axios({
              headers: {
                  'Content-type': 'application/json; charset=UTF-8',
              },
              method: 'post',
              url: 'https://jsonplaceholder.typicode.com/posts',
              data: JSON.stringify({
                  title: 'foo',
                  body: 'bar',
                  userId: userId
              })
              // data: {a: count}
          }).then((res: any) => {
              console.log('res', res);
              return []
          });
          setData(result)
      }
      fetchData()
  }, [userId]);
  1. 如果需要state且这个方法不需要被复用.把这个方法提出去,然后把state作为参数传递给这个方法。然后把这个方法放到useEffect中,最后把这个state作为依赖
// 这个方法放到组件外部
const fetchData = async (id: number) => {
    const result = await Axios({
        headers: {
            'Content-type': 'application/json; charset=UTF-8',
        },
        method: 'post',
        url: 'https://jsonplaceholder.typicode.com/posts',
        data: JSON.stringify({
            title: 'foo',
            body: 'bar',
            userId: id
        })
        // data: {a: count}
    }).then((res: any) => {
        console.log('res', res);
        return []
    });
    // setData(result)
    return result
}

useEffect(() => {       
        fetchData(userId)
    }, [userId]);
  1. 需要state,且这个方法在组件中还有别的地方调用。可以在组件中生命这个方法,且使用useCallback包裹着,并让使用的state作为useCallback的依赖项,最后在useEffect中调用这个方法,并使这个方法称为useEffect的依赖。
    import React, {
    useCallback,
    useEffect,
    useState
} from 'react';
import Axios from 'axios';
const Dashboard = () => {
    const [userId, setUserId] = useState(60);
    const [data, setData] = useState<any[]>([]);
    const fetchData = useCallback(async () => {
        const result = await Axios({
            headers: {
                'Content-type': 'application/json; charset=UTF-8',
            },
            method: 'post',
            url: 'https://jsonplaceholder.typicode.com/posts',
            data: JSON.stringify({
                title: 'foo',
                body: 'bar',
                userId: userId
            })
            // data: {a: count}
        }).then((res: any) => {
            console.log('res', res);
            return []
        });
        setData(result)
    }, [userId])

    useEffect(() => {
        fetchData()
    }, [fetchData]);
    return (
        <>
            ---{userId}---
        </>
    );
}

export default Dashboard

其实函数作为依赖,主要是得注意函数每次状态更新都会生成不一样的函数,每次不一样的话相当于这个依赖无效,所以要保证函数的不变性。

场景四:props作为依赖

prop作为依赖其实和state一样的,按照上述的解决办法就行。

总结

个人感觉useEffect还不够智能,不能实现不需要使用依赖就能自己帮我们做处理,必须我们自己去设置。不过具体为什么这么去实现,我还没有深究。以上也是我在使用useEffect过程中遇到的问题,以及解决办法,我也不需要去理解useEffect,会用就行。ps:理解了还是会忘记,烦!!!