React18 (四) Context,Effect,Reducer和Memo

代碼地址:https://github.com/showkawa/react18-ZeroToOne/tree/main/react03

1.Context

在React中組件間的數據通信是通過props進行的,父組件給子組件設置props,子組件給後代組件設置props,props在組件間自上向下逐層傳遞數據,層級變多了維護起來很麻煩。有些數據需要誇多個組件中共同使用,props傳遞的方式已經不適用了。
Context爲我們提供了一種在不同組件間共享數據的方式,在外層組件中統一設置,設置後內層所有的組件都可以訪問到Context中所存儲的數據。Context類似於JS中的全局作用域,可以將一些公共數據設置到一個同一個Context中,使得所有的組件都可以訪問到這些數據。創建Context:

const GlobalContext = React.createContext(defaultValue);

React.createContext(defaultValue)用來創建一個Context對象,它需要一個初始值作爲參數,這個初始值可以是一個原始值,也可以是一個JS對象。調用以後,方法將會返回一個Context對象,當我們想在其他組件中訪問Context中的數據時,必須要通過這個對象。由於Context對象需要在不同的組件中被使用,所以通常我們會將Context對象設置到一個單獨的模塊中並設置爲默認導出,例如下面的樣例代碼:

import React from 'react';

const CartContext = React.createContext({
    items: [],
    totalAmount: 0,
    totalPrice: 0,
    // addItem: () => { },
    // removeItem: () => { },
    // clearItem: () => { },
    cartsDispatch: () => { },
});

export default CartContext;

React爲我們提供了一個鉤子函數`useContext()`,我們只需要將Context對象作爲參數傳遞給鉤子函數,它就會直接給我們返回Context對象中存儲的數據。

import classes from './Cart.module.css';
import iconImg from '../../../asset/bag.png';
import CartContext from '../../../store/CartContext';
import { useCallback, useContext, useEffect, useState } from 'react';
import CartDetails from './CartDetails/CartDetails';
import Checkout from './Checkout/Checkout';

const Cart = () => {

    const ctx = useContext(CartContext);

    const [showDetails, setShowDetails] = useState(false);

    const [showCheckout, setShowCheckout] = useState(false);

    const toggleDetailsHandler = (e) => {
        if (ctx.totalAmount === 0) { return }
        setShowDetails(preValue => !preValue);
    }

    const showCheckoutHandler = useCallback(() => {
        if (ctx.totalAmount === 0) { return }
        setShowCheckout(true);
    },[ctx]);


    const hideCheckoutHandler = (e) => {
        e.stopPropagation();
        setShowCheckout(false);
    }

    useEffect(() => {
        if (ctx.totalAmount === 0) {
            setShowDetails(false);
            setShowCheckout(false);
        }
    }, [ctx])



    return (
        <div className={classes.Cart} onClick={toggleDetailsHandler}>
            {showCheckout && <Checkout onHide={hideCheckoutHandler} />}
            {showDetails && <CartDetails />}

            <div className={classes.Icon}>
                <img src={iconImg} alt="bag" />
                {
                    ctx.totalAmount === 0 ? null : <span className={classes.TotalAmount}>{ctx.totalAmount}</span>
                }
            </div>
            {
                ctx.totalPrice === 0 ? <p className={classes.NoMeal}>請選擇商品</p> : <p className={classes.Price}>{ctx.totalPrice}</p>
            }
            <button className={`${classes.Button} ${ctx.totalPrice === 0 ? classes.Disable : ''}`} onClick={showCheckoutHandler}>去結算</button>
        </div>)
        ;
}

export default Cart;

有了消費方的組件就需要提供方,React提供了Provider,用來在組件中指定Context值,如樣例代碼我們在App.js中用<CartContext.Provider> 根元素,提供一種全局的共享對象

function App() {
  ......return (
    <CartContext.Provider value={{ ...carts, cartsDispatch }}>
    ......
    </CartContext.Provider>
  );
}

export default App;

Provider譯爲生產者,和Consumer消費者對應。Provider會設置在外層組件中,通過value屬性來指定Context的值。Context的搜索流程和JS中函數作用域類似,當我們獲取Context時,React會在它的外層查找最近的Provider,然後返回它的Context值。如果沒有找到Provider,則會返回Context模塊中設置的默認值。

2.Effect

React組件有部分邏輯如果直接寫在函數體中,會影響到組件的渲染,這部分會產生“副作用”的代碼,是一定不能直接寫在函數體中。例如,如果直接將修改state的邏輯編寫到了組件之中,就會導致組件不斷的循環渲染,直至調用次數過多內存溢出。

React.StrictMode

編寫React組件時,我們要極力的避免組件中出現那些會產生“副作用”的代碼。同時,如果你的React使用了嚴格模式,也就是在React中使用了React.StrictMode標籤,那麼React會非常“智能”的去檢查你的組件中是否寫有副作用的代碼。React的嚴格模式,在處於開發模式下,會主動的重複調用一些函數,以使副作用顯現。所以在處於開發模式且開啓了React嚴格模式時,這些函數會被調用兩次:

類組件的的 constructor, render, 和 shouldComponentUpdate 方法
類組件的靜態方法 getDerivedStateFromProps
函數組件的函數體
參數爲函數的setState
參數爲函數的useState, useMemo, or useReducer

重複的調用會使副作用更容易凸顯出來,你可以嘗試着在函數組件的函數體中調用一個console.log你會發現它會執行兩次,如果你的瀏覽器中安裝了React Developer Tools,第二次調用會顯示爲灰色。

使用Effect

React專門爲我們提供了鉤子函數useEffect(),專門用來處理那些不能直接寫在組件內部的代碼。哪些代碼不能直接寫在組件內部呢?像是:獲取數據、記錄日誌、檢查登錄、設置定時器等。簡單來說,就是那些和組件渲染無關,但卻有可能對組件產生副作用的代碼。

useEffect語法:

useEffect(()=>{
    /* 編寫那些會產生副作用的代碼 */
});

useEffect()中的回調函數會在組件每次渲染完畢之後執行,這也是它和寫在函數體中代碼的最大的不同,函數體中的代碼會在組件渲染前執行,而useEffect()中的代碼是在組件渲染後才執行,這就避免了代碼的執行影響到組件渲染。

通過使用這個Hook,我設置了React組件在渲染後所要執行的操作。React會將我們傳遞的函數保存(我們稱這個函數爲effect),並且在DOM更新後執行調用它。React會確保effect每次運行時,DOM都已經更新完畢。

清除Effect

組件的每次重新渲染effect都會執行,有一些情況裏,兩次effect執行會互相影響。比如,在effect中設置了一個定時器,總不能每次effect執行都設置一個新的定時器,所以我們需要在一個effect執行前,清除掉前一個effect所帶來的影響。要實現這個功能,可以在effect中將一個函數作爲返回值返回,像是這樣:

useEffect(()=>{
    /* 編寫那些會產生副作用的代碼 */
    
    return () => {
        /* 這個函數會在下一次effect執行前調用 */
    };
});

effect返回的函數,會在下一次effect執行前調用,我們可以在這個函數中清除掉前一次effect執行所帶來的影響。

限制Effect

組件每次渲染effect都會執行,這似乎並不總那麼必要。因此在useEffect()中我們可以限制effect的執行時機,在useEffect()中可以將一個數組作爲第二個參數傳遞,像是這樣:

useEffect(()=>{
    /* 編寫那些會產生副作用的代碼 */

    return () => {
        /* 這個函數會在下一次effect執行前調用 */
    };
}, [a, b]);

上例中,數組中有兩個變量a和b,設置以後effect只有在變量a或b發生變化時纔會執行。這樣即可限制effect的執行次數,也可以直接傳遞一個空數組,如果是空數組,那麼effect只會執行一次。

例如下面的搜索組件,input框輸入的keyword變化時,就會觸發useEffect,只是過濾方法會在500毫秒後在執行,只要在500毫秒內有不間斷的輸入變化,就可以看到防抖效果,而不是每次輸入變化都會觸發過濾方法 

import React, { useEffect, useState } from "react";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import classes from './FilterMeals.module.css';

const FilterMeals = (props) => {

    const [keyword, setKeyword] = useState('');

    const inputChangeHandler = e => {
        setKeyword(e.target.value.trim());

    }

    // 搜索防抖
    useEffect(() => {
        const timer = setTimeout(() => {
            console.log('--- fliter ---')
            props.onFilter(keyword);
        }, 500);

        return () => {
            console.log('--- return filter ---')
            clearTimeout(timer);
        }
    }, [keyword])


    return (<div className={classes.FilterMeals}>
        <div className={classes.InputOuter}>
            <input
                value={keyword}
                className={classes.SearchInput}
                type="text"
                placeholder={"請輸入關鍵字"}
                onChange={inputChangeHandler}
            />
            <FontAwesomeIcon className={classes.SearchIcon} icon={faSearch} />
        </div>
    </div>);
}

export default FilterMeals;

3.Reducer

在React的函數組件中,我們可以通過useState()來創建state。但是這種方式也存在着一些不足,因爲所有的修改state的方式都必須通過setState()來進行,如果遇到一些複雜度比較高的state時,這種方式似乎就變得不是那麼的優雅。爲了解決複雜State帶來的不便,React爲我們提供了一個新的使用State的方式。Reducer橫空出世,reduce單詞中文意味減少,它的作用就是將那些和同一個state相關的所有函數都整合到一起,方便在組件中進行調用。

和State相同Reducer也是一個鉤子函數,語法如下:

const [state, dispatch] = useReducer(reducer, initialArg, init);

它的返回值和useState()類似,第一個參數是state用來讀取state的值,第二個參數同樣是一個函數,不同於setState()這個函數我們可以稱它是一個“派發器”,通過它可以向reducer()發送不同的指令,控制reducer()做不同的操作。

它的參數有三個,第三個我們暫且忽略,只看前兩個。reducer()是一個函數,也是我們所謂的“整合器”。它的返回值會成爲新的state值。當我們調用dispatch()時,dispatch()會將消息發送給reducer(),reducer()可以根據不同的消息對state進行不同的處理。initialArg就是state的初始值,和useState()參數一樣。樣例代碼如下:

import './App.css';
import React, { useReducer, useState } from 'react'
import CartContext from './store/CartContext';
import { Route } from 'react-router-dom';
import Home from './component/Home/Home';
import Shopping from './component/Shopping/Shopping';
import Member from './component/Member/Member';
import Bonus from './component/Bonus/Bonus';
import BottomNavigation from '@mui/material/BottomNavigation';
import BottomNavigationAction from '@mui/material/BottomNavigationAction';
import RestoreIcon from '@mui/icons-material/Restore';
import FavoriteIcon from '@mui/icons-material/Favorite';
import ArchiveIcon from '@mui/icons-material/Archive';
import Paper from '@mui/material/Paper';
import { Link, Redirect, Switch, useHistory, useLocation } from 'react-router-dom/cjs/react-router-dom.min';
import Wechat from './component/Bonus/Wechat/Wechat';

const cartsReducer = (params, action) => {
  const newCarts = { ...params };
  switch (action.type) {

    case 'ADD':
      if (newCarts.items.indexOf(action.meal) === -1) {
        newCarts.items.push(action.meal);
        action.meal.attributes.amount = 1;
      } else {
        action.meal.attributes.amount += 1;
      }
      newCarts.totalAmount += 1;
      newCarts.totalPrice += action.meal.attributes.price;
      return newCarts;

    case 'REMOVE':
      if (--action.meal.attributes.amount <= 0) {
        newCarts.items.splice(newCarts.items.indexOf(action.meal), 1);
      }
      newCarts.totalAmount -= 1;
      newCarts.totalPrice -= action.meal.attributes.price;
      return newCarts;

    case 'CLEAR':
      newCarts.items.forEach(item => delete item.attributes.amount);
      newCarts.items = [];
      newCarts.totalAmount = 0;
      newCarts.totalPrice = 0;
      return newCarts;

    default:
      return params;
  }
}

function App() {

  const [carts, cartsDispatch] = useReducer(cartsReducer, {
    items: [],
    totalAmount: 0,
    totalPrice: 0
  });

  const [value, setValue] = useState('/');

  const _history = useHistory();

  const gotoHander = (event, newValue) => {
    setValue(newValue);
    _history.push(newValue);
  }

  const homeProps = {
    name: 'hanbao'
  }

  const memberProps = {
    name: 'member',
    params: {}

  }

  return (
    <CartContext.Provider value={{ ...carts, cartsDispatch }}>

      {/* <Route exact path = '/' component ={Home}/> */}
      <Route exact path='/' children={<Home {...homeProps} />} />
      <Route path='/bonus' component={Bonus} />
      <Switch>
        <Route exact path='/bonus/wechat' component={Wechat} />
      </Switch>
      <Route exact path='/shopping' component={Shopping} />
      <Route exact path='/home' component={Home} />
      <Route path='/member' render={() => <Member {...memberProps} />} />


      <Paper sx={{ position: 'fixed', bottom: 0, left: 0, right: 0 }} elevation={1}>
        <BottomNavigation
          showLabels
          value={value}
          onChange={gotoHander}
        >
          <BottomNavigationAction label="Home" value="/" icon={<RestoreIcon />} />
          <BottomNavigationAction label="Bonus Plan" value="/bonus" icon={<FavoriteIcon />} />
          <BottomNavigationAction label="Store" value="/shopping" icon={<ArchiveIcon />} />
          <BottomNavigationAction label="Member" value={`/member/${homeProps.name}`} icon={<ArchiveIcon />} />
        </BottomNavigation>
      </Paper>
    </CartContext.Provider>

  );
}

export default App;

在其他組件中,需要操作state時,只需先獲取CartContext然後通過ctx.cartDispath操作購物車:

const ctx = useContext(CartContext); // 加載context
ctx.cartDispatch({type:'ADD_ITEM', meal:props.meal}); // 添加操作
ctx.cartDispatch({type:'REMOVE_ITEM', meal:props.meal}); // 刪除操作

4.Memo

React組件會在兩種情況下發生重新渲染。第一種,當組件自身的state發生變化時。第二種,當組件的父組件重新渲染時。第一種情況下的重新渲染無可厚非,state都變了,組件自然應該重新進行渲染。但是第二種情況似乎並不是總那麼的必要。

 

三個組件的引用關係爲,A組件是App的子組件、B組件是A組件的子組件:App –> A –> Bar

當App組件發生重新渲染時,A和Bar組件都會發生重渲染。當A組件重新渲染時,Bar組件也會重新渲染。假如Bar組件中沒有state,甚至連props都沒有設置。換言之,Bar組件無論如何渲染,每次渲染的結果都是相同的,雖然重渲染並不會應用到真實DOM上,但很顯然這種渲染是完全沒有必要的。爲了減少像Bar組件這樣組件的渲染,React爲我們提供了一個方法React.memo()。該方法是一個高階函數,可以用來根據組件的props對組件進行緩存,當一個組件的父組件發生重新渲染,而子組件的props沒有發生變化時,它會直接將緩存中的組件渲染結果返回而不是再次觸發子組件的重新渲染,這樣一來就大大的降低了子組件重新渲染的次數。

樣例如下修改:

import React from 'react';
import classes from './Bar.module.css';

const Bar = (props) => {

    console.log('--- Bar Component ---', props);
    return (
        <div className={classes.Bar}>
            <div className={classes.TotalPrice}>{props.totalPrice}</div>
            <button className={classes.Button}>去支付</button>
        </div>
    );
}

export default React.memo(Bar);

 

修改後的代碼中,並沒有直接將Bar組件向外導出,而是在Bar組件外層套了一層函數React.memo(),這樣一來,返回的Bar組件就增加了緩存功能,只有當Bar組件的props屬性發生變化時,纔會觸發組件的重新渲染。memo只會根據props判斷是否需要重新渲染,和state和context無關,state或context發生變化時,組件依然會正常的進行重新渲染。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章