2021年7月22日 星期四

[useCallback, react, fun comp, hook] 如何使用 useCallback 更新 React function component 的狀態

如何使用 useCallback 更新 React function component 的狀態 

井民全, Jing, mqjing@gmail.com



這份文件主要是閱讀 Jan Hesters 先生的  useCallback vs. useMemo 這篇基礎文章心得

Q: 為何討論這個?

==> function component 每次被 render 時, 裡面的 innfer function 都會被重新建立, 這造成了 referential inequality. 如果你使用 useEffect 來更新 side effect 狀態. 會因為這個 referential inequality 造成無謂的更新. 沒弄好, 還會造成無限循環呼叫. 造成效能重大的損失.

問題描述

下面的 React 程式碼會造成無限循環呼叫的問題: 就算是 userId 沒有變動也會執行 useEffect 裡面的 fetchUser, 而且是無限循環.

概念:

useEffect -> fetchUser -> render -> useEffect -> fetchUser -> render ....


Github: Source

Code

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

import ListItem from '@material-ui/core/ListItem';

import ListItemText from '@material-ui/core/ListItemText';


function User({ userId }) {

  const [user, setUser] = useState({ name: '', email: '' });


  // 更新 user工作 function (每次 render 都會重新 create function)

  const fetchUser = async () => {

    const res = await fetch(

      `https://jsonplaceholder.typicode.com/users/${userId}`

    );

    const newUser = await res.json();

    setUser(newUser);    // <-- 會產生 render 事件

  };


  useEffect(() => {

    fetchUser();

  }, [fetchUser]);   // <<---- 因為每次 render 後, fetchUser 的 reference 都不一樣,         

                              //           因此就算是 userId 沒有變動也會執行 useEffect 裡面的 fetchUser. 

                              //           而且是無限循環.


  return (

    <ListItem dense divider>

      <ListItemText primary={user.name} secondary={user.email} />

    </ListItem>

  );

}


export default User;


原因

造成無限循環呼叫, 主要是因為放在 function component 中的  inner function 變數 fetchUser, 每次在這個 component 重新繪製時, 都會重新被建立. 這造成了 fetchUser 的內容產生  referential inequality. 因此, 使得條件式 useEffect 不斷的被呼叫. 細節可以參考 Closure.


解決無限循環呼叫問題

方法一: 把 fetchUser 移到 useEffect 裡面, 然後由 userId 這個不變值設定呼叫條件

因為 userID 內容不會變動, 所以不會造成前面不斷呼叫 fetchUser 無限迴圈的問題.

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

import ListItem from '@material-ui/core/ListItem';

import ListItemText from '@material-ui/core/ListItemText';


function User({ userId }) {

    const [user, setUser] = useState({ name: '', email: '' });



    useEffect(() => {

        // 宣告更新 user工作 function (每次 render 都會重新 intiial)

        const fetchUser = async () => {

            const res = await fetch(

                `https://jsonplaceholder.typicode.com/users/${userId}`

            );

            const newUser = await res.json();

            setUser(newUser);    // <-- 會產生 render 事件

        };


    }, [userID]);   // <<---- 因為 userID 不會變動, 因此 fetchUser 只會呼叫一次

    


    return (

        <ListItem dense divider>

            <ListItemText primary={user.name} secondary={user.email} />

        </ListItem>

    );

}


export default User;





如果你不想把 fetchUser 放在 inner function 裡面, 希望到處都能使用它. 可以用 useCallback, 把要更新狀態的 function 包裝成 memorized callback function. 因為 useCallback 會把你的 function 包裝成 memerized function, 當條件不變時, 完全不會改變回傳的 function. 因此, 可以把 useCallback 回傳的 function 就進 useEffect 裡面.


方法二: 使用 useCallback 的方式包裝 fetchUser

根據定義 useCallback

useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed.

所以, 雖然 function component 每次被重新 render 時, 都會再執行一次

  const fetchUser = useCallback(async () => {

       ...

  }, [props.userId]);   

但如果傳進來的 props.userID 沒有改變.  由 useCallack 回傳的 memorized 版本 fetchUser 內容也不會改變. 


因此, 這樣做才可以在只有 userID 改變時, 才執行後續工作. 此處指的是, 只有在 userID 改變時, 才執行 useEffect 裡面的工作. 到網路上去取得 user list.


概念:

userID (不變) --> fetchUser (memorized version 不變) -> useEffect (放心的 update).

GitHub: Source

Code

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

import ListItem from '@material-ui/core/ListItem';

import ListItemText from '@material-ui/core/ListItemText';


function User(props:{userId:string }):: JSX.Element  {

  const [user, setUser] = useState({ name: '', email: '' });


  // 宣告更新 user工作 function (用 useCallback 包裝成  memorized callback function)

  const fetchUser = useCallback(async () => {

      const res = await fetch(

            `https://jsonplaceholder.typicode.com/users/${props.userId}`

      );

      const newUser = await res.json();

      setUser(newUser);    // <-- 會產生 render 事件

   }, [props.userId]);     // <<---- 因為 userID 不會變動, 因此 fetchUser 的內容不會變動 

                          // 即 featchUser 回傳的 callback function 不會改變內容.



  useEffect(() => {

    fetchUser();      

  }, [fetchUser]);   // <<---- 因為 fetchUser 沒有變動, 所以 fetchUser 只會在 userId 變換時, 才呼叫


  return (

    <ListItem dense divider>

      <ListItemText primary={user.name} secondary={user.email} />

    </ListItem>

  );

}


export default User;







function Blub() {

const bar = React.useCallback(() => {}, [])

const baz = React.useMemo(() => [1, 2, 3], [])

return <Foo bar={bar} baz={baz} />

}





References

  1. https://medium.com/@jan.hesters/usecallback-vs-usememo-c23ad1dc60

  2. https://kentcdodds.com/blog/usememo-and-usecallback