Try   HackMD

React-Hooks or function

紀錄一些比較少用到的 hooks 或函式,有些是在課程
https://www.udemy.com/course/react-the-complete-guide-incl-redux/?couponCode=LETSLEARNNOW 中學到

useContext

可以想像成是一個全域變數,被這個 context 的 provider 包覆住的元件(包含元件內的子元件)都可以使用這裏面的內容,最常被使用在是傳入一些 state 和修改 state 的函式,可以用來避免 props drilling的情況。

props drilling 的情況就是假如有一個變數或函式要在比較內層的子物件中使用,可能就必須透過 props 來一層層傳遞到下層,即使中間這些傳遞的物件並沒有使用到這裏面的變數或函式。這樣不僅不直觀,也會讓整個 props 變的冗長。

使用方法:

首先,要先創建一個 context ,一般會建議新增一個檔案然後把這個 context export 出去。

例如:

import { createContext, useContext } from "react";

export const CartContext = createContext({
  items: [],
  addItemToCart: ()=>{}
})

接下來,將這個 context import 後,在最外層的地方使用 provider 包覆,例如:

const ctxValue = {
    items:shoppingCart.items,
    addItemToCart: handleAddItemToCart
  };

  return (
    <CartContext.Provider value = {ctxValue}>
      <Header
        cart={shoppingCart}
        onUpdateCartItemQuantity={handleUpdateCartItemQuantity}
      />
      <Shop onAddItemToCart={handleAddItemToCart} />
    </CartContext.Provider>
  );

記得要加入 provider 和 value,如果不加 provider 可能會讀到預設值,不加 value 會讀到 undefined。
參考: https://react.dev/reference/react/useContext

接下來,內部的元件如果要使用這個 context ,有兩種方法:

  1. 透過 useContext 來使用 (推薦):
    剛才已經透過 provider 提供了 context,接下來就可以透過
  const {addItemToCart} = useContext(CartContext)
  return (
    <article className="product">
      <img src={image} alt={title} />
      <div className="product-content">
        <div>
          <h3>{title}</h3>
          <p className='product-price'>${price}</p>
          <p>{description}</p>
        </div>
        <p className='product-actions'>
          <button onClick={() => addItemToCart(id)}>Add to Cart</button>
        </p>
      </div>
    </article>
  );

來使用 context 的內容

此處透過解構來使用,當然要寫成 useContext 獲取整個 CartContext 的內容再透過 .addItemToCart 使用也可以

  1. 透過 consumer 使用:
    另一種方法是透過 consumer 使用,但這樣寫可能會讓程式比較難讀。

記住在 Consumer 內必須有一個函式回傳內容,而 cartCtx 就是獲取的 context。

return (
    <CartContext.Consumer>
      {(cartCtx) => {
        return (
          <article className="product">
          <img src={image} alt={title} />
          <div className="product-content">
            <div>
              <h3>{title}</h3>
              <p className='product-price'>${price}</p>
              <p>{description}</p>
            </div>
            <p className='product-actions'>
              <button onClick={() => cartCtx.addItemToCart(id)}>Add to Cart</button>
            </p>
          </div>
        </article>
        )
      }}
    </CartContext.Consumer>
    
  );

可以看到為了使用會變得比較冗長,因此一般還是建議使用 useContext。

useReducer

可以想成用來操作比較複雜的邏輯時可以使用的 state,舉例來說:

import React from 'react;'


function App() {
    
    const [count,dispatch] = React.useReducer(counterReducer,0);
    
  return (
    <div id="app">
      <h1>The (Final?) Counter</h1>
      <p id="actions">
        <button onClick = {()=>dispatch("increase")}>Increment</button>
        <button onClick = {()=>dispatch("decrease")}>Decrement</button>
        <button onClick = {()=>dispatch("reset")}>Reset</button>
      </p>
      <p id="counter">{count}</p>
    </div>
  );
}

export default App;

首先要宣告一個 function ,這個 function 是用來定義當接收到 action 時,應該做甚麼動作,在這個範例中就是 counterReducer。

export function counterReducer(state, action) {
    if(action === "increase"){
        return state + 1;
    }
    if(action === "decrease"){
        return state - 1;
    }
    if(action === "reset"){
        return 0;
    }
    return state;
}

使用時類似 useState,useReducer 包含兩個參數,第一個是前面定義行為的function,第二個是初始值。

const [count,dispatch] = React.useReducer(counterReducer,0);

接下來,假如想使用這個 reducer(例如按下按鈕 +1):

<button onClick = {()=>dispatch("increase")}>increase</button>

dispatch function 類似於 setState,但傳入的是要執行的行為,如果透過 useState,可能必須分開寫成三個函式,分別是:

setCount(prev=>prev + 1)
setCount(prev=>prev - 1)
setCount(0)

儘管實際寫的程式碼沒有變少,但可以讓程式碼的可讀性提高。

memo

提到 memo 之前,要先說明 react 中各種元件更新的方法:

首先,component 中如果又有其他 custom component,這樣就會產生樹狀的結構,舉例來說:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

這張圖中, App component 又用了兩個 custom component,而這些 custom component 又用了其他的 compnent。

假設此時 APP component 中的 state 變動,這會讓 APP component 和他的兩個子元件重新渲染。

但考慮到另一個情況,假如 App component 的變動實際上和他的子元件沒有關係,但這些元件還是會被重新渲染,這明顯不太符合效益。

也因此出現了 memo,這是 react 中提供的函式,用法是:

Counter = memo(function Counter({ initialCount }) {
  //...return jsx
});

export default Counter;

實際上就是用 memo 把原本的 function component 包起來而已,使用 memo 的function component 會在 parent component 重新渲染時檢查自己的 props 是否被變動,如果有變動,function component 才會重新被執行。

聽起來那好像把所有 component 都用 memo 包起來效能會最好,但實際上,用了 memo 相當於每次都要對 props 進行比對,實際上也會對效能造成影響,因此使用 memo 時要注意下面幾點:

  1. 如果是會頻繁被 parent component 影響的 component,那不要使用 memo,否則已經要頻繁更新了,還要浪費效能去進行比對。
  2. 盡可能在 tree 的上層使用,因為上層不更新時,下面的 component 也不會更新。
  3. memo 只針對外部,component 自己的 state 更新時當然還是會重新渲染。

useCallback

前面講到 memo 可以避免在 props 沒有改變時還重新渲染的情況,但有一點要注意,就是如果傳入的 props 是一個函式,那這個函式會在重新被渲染時再次創建,這會導致其 child component 即使加上了 memo 還是被重新渲染,因為函式記憶體位置變了。

要解決這個問題,可以透過 useCallback,使用方法類似 useEffect:

const func = React.useCallback(function(){
    console.log("strong");
},[a,b])

這個 function 在 a,b 沒有被改變時不會被重新創建,記憶體位置也不會變動,如此一來就可以避免前面提到的問題。

useMemo

useMemo 是用來阻止複雜的運算,以下是一個範例,假設下面的程式碼是在一個 component 內,n 是某個 props的內容:

const num = complex_function(n); 

如果這個 complex_function 的運算很複雜,當然會希望避免重複運算,因此可以透過 useMemo:

const num = useMemo(()=>complex_function(n),[n])

和 useCallback、useEffect 相同,useMemo 第一個參數是一個有回傳值的函式,第二個則是 dependency array,裡面的內容有變才會被重新運算。

createPortal

這是一個在 react-dom 中的函式,最常被應用的地方是彈窗,語法為:

createPortal(children, domNode, key?)

這允許這個 component 出現在 dom-tree 的任何地方,而不只是包含在當前這個 component。

舉例來說,如果想要有一個彈出視窗出現在最外面,可以使用:

{showModal && createPortal(
        <ModalContent onClose={() => setShowModal(false)} />,
        document.body
      )}

這就代表把 ModalContent 這個 component 插入到元素中(如果要插入在別的位置,可以用 getElementByID)。

另外可以搭配 dialog 標籤使用,例如

function Modal({ open, children, onClose }) {
  const dialog = useRef();

  useEffect(() => {
    if (open) {
      dialog.current.showModal();
    } else {
      dialog.current.close();
    }
  }, [open]);

  return createPortal(
    <dialog className="modal" ref={dialog} onClose={onClose}>
      {open ? children : null}
    </dialog>,
    document.getElementById('modal')
  );
}

這會把這個 component 插入到 modal 這個 dom 內,並且可以透過 showModal 把不能訪問的部分變暗。