react hooks使用指北

一、react新特性

1. context

在一個典型的 React 應(yīng)用中,數(shù)據(jù)是通過 props 屬性自上而下(由父及子)進行傳遞的,但這種做法對于某些類型的屬性而言是極其繁瑣的(例如:地區(qū)偏好,UI 主題),這些屬性是應(yīng)用程序中許多組件都需要的。Context 提供了一種在組件之間共享此類值的方式,而不必顯式地通過組件樹的逐層傳遞 props。

來看一個例子,首先來創(chuàng)建一個context

// context.js
import { createContext } from 'react'

export const TrainContext = createContext()

然后在app.jsx中引入

// app.jsx
import { TrainContext } from './context'
<TrainContext.Provider
   value={{
      trainNumber,
      departStation,
      arriveStation,
      departDate,
    }}
>
  <Candidate tickets={tickets} />
</TrainContext.Provider>

channel是candidate的一個子組件,我們在channel中通過useContext獲取context中的值

const {
    trainNumber,
    departStation,
    arriveStation,
    departDate,
  } = useContext(TrainContext)

現(xiàn)在這些值就可以在組件中使用了。

2. 異步加載組件

我們配合Suspense使用

import React, { lazy, Suspense } from 'react';

const About = lazy(() => import('./About')) // 延遲(異步)加載

function App() {
  return (
    <div>
      <Suspense fallback={<div>loading</div>}>
        <About />
      </Suspense>
    </div>
  );
}

3. Memo

React.memo高階組件。它與 React.PureComponent 非常相似,但它適用于函數(shù)組件,但不適用于 class 組件。

如果你的函數(shù)組件在給定相同 props 的情況下渲染相同的結(jié)果,那么你可以通過將其包裝在 React.memo 中調(diào)用,以此通過記憶組件渲染結(jié)果的方式來提高組件的性能表現(xiàn)。這意味著在這種情況下,React 將跳過渲染組件的操作并直接復(fù)用最近一次渲染的結(jié)果。

默認情況下其只會對復(fù)雜對象做淺層對比,如果你想要控制對比過程,那么請將自定義的比較函數(shù)通過第二個參數(shù)傳入來實現(xiàn)。

二、項目配置

1. eslint配置

使用eslint hooks可以幫我們檢查使用hooks過程中出現(xiàn)的錯誤,使用步驟如下:
安裝eslint-plugin-react-hooks

npm install eslint-plugin-react-hooks --save-dev

然后在package.json中添加

"plugins": [
      "react-hooks"
],
"rules": {
      "react-hooks/rules-of-hooks": "error"
}

如下圖所示:


image.png

2. react多頁應(yīng)用的webpack配置

我們使用create-react-app腳手架生成項目,然后對配置內(nèi)容做一些改動,使它支持多頁應(yīng)用。
首先在path.js中添加多頁的路徑和模版html路徑

  appHtml: resolveApp('public/index.html'),
  appQueryHtml: resolveApp('public/query.html'),
  appOrderHtml: resolveApp('public/order.html'),
  appTicketHtml: resolveApp('public/ticket.html'),
  appIndexJs: resolveModule(resolveApp, 'src/index/'),
  appOrderJs: resolveModule(resolveApp, 'src/order/'),
  appQueryJs: resolveModule(resolveApp, 'src/query/'),
  appTicketJs: resolveModule(resolveApp, 'src/ticket/'),

然后在entry中設(shè)置多入口打包

entry: {
      index: [paths.appIndexJs, isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean),
      qusry: [paths.appQueryJs, isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean),
      ticket: [paths.appTicketJs, isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean),
      order: [paths.appOrderJs, isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean),
}

接下來對每個頁面設(shè)置模版,這里以query.html為例

new HtmlWebpackPlugin(
        Object.assign(
          {},
          {
            inject: true,
            template: paths.appQueryHtml,
            filename: 'query.html',
            chunks: ['query']
          },
          isEnvProduction
            ? {
                minify: {
                  removeComments: true,
                  collapseWhitespace: true,
                  removeRedundantAttributes: true,
                  useShortDoctype: true,
                  removeEmptyAttributes: true,
                  removeStyleLinkTypeAttributes: true,
                  keepClosingSlash: true,
                  minifyJS: true,
                  minifyCSS: true,
                  minifyURLs: true,
                },
              }
            : undefined
      )
)

我們還可以設(shè)置打包分析,在webpack.config.js中引入webpack-bundle-analyzer

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // 打包分析

// 添加到plugins中
process.env.GENERATE_BUNDLE_ANALYZER === 'true' &&
        new BundleAnalyzerPlugin({
          openAnalyzer: false, // 禁止打開服務(wù)器
          analyzerMode: 'static', // 生成靜態(tài)html文件
        })

還可以設(shè)置cdn路徑,這里有兩種方法,一種是我們在執(zhí)行打包命令時添加cdn地址:

PUBLIC_URL=https://www.cdn.baidu.com/ npm run build

第二種需要我們修改配置文件
當我們在output中設(shè)置publicPath時,根據(jù)環(huán)境設(shè)置不同的路徑

publicPath: 'production' !== process.env.NODE_ENV || 'true' === process.env.USE_LOCAL_FILES
          ? '/'
          : 'https://www.cdn.baidu.com/',

設(shè)置代理,我們在package.json中配置

"proxy": "http://localhost:5555",

三、 redux

之前的redux創(chuàng)建可參考react進階
我們來創(chuàng)建store.js

import { createStore, combineReducers, applyMiddleware } from 'redux'

import reducers from './reducers'
import thunk from 'redux-thunk'

export default createStore(
  combineReducers(reducers),
  {
    departDate: Date.now(),
    arriveDate: Date.now(),
    departTimeStr: null, // 到達時間小時
    arriveTimeStr: null,
  },
  applyMiddleware(thunk)
)

createStore接受3個參數(shù):reducer, preloadedState, enhancer。第二個參數(shù)是preloadedState,它是state的初始值,如果我們第二個參數(shù)是函數(shù)類型,createStore會認為你忽略了preloadedState而傳入了一個enhancer,如果我們傳入了一個enhancer,createStore會返回enhancer(createStore)(reducer, preloadedState)的調(diào)用結(jié)果,這是常見高階函數(shù)的調(diào)用方式。在這個調(diào)用中enhancer接受createStore作為參數(shù),對createStore的能力進行增強,并返回增強后的createStore。然后再將reducer和preloadedState作為參數(shù)傳給增強后的createStore,最終得到生成的store。

redux-thunk的作用是使redux支持異步action(先獲取當前值,再做一些修改)

Redux中負責響應(yīng)action并修改數(shù)據(jù)的角色就是reducer,reducer的本質(zhì)實際上是一個函數(shù),其函數(shù)簽名為:reducer(previousState,action) => newState??梢钥闯觯瑀educer 接受兩個參數(shù),previousState以及action函數(shù)返回的action對象,并返回最新的state。來創(chuàng)建reducers.js

export default {
  departDate(state = Date.now(), action) {
    const { type, payload } = action
    switch (type) {
      case ACTION_SET_DEPART_DATE:
        return payload
      default:
    }

    return state
  },
  arriveDate(state = Date.now(), action) {
    const { type, payload } = action
    switch (type) {
      case ACTION_SET_ARRIVE_DATE:
        return payload
      default:
    }

    return state
  },
  departTimeStr(state = null, action) {
      const { type, payload } = action
      switch (type) {
        case ACTION_SET_DEPART_TIME_STR:
          return payload
        default:
      }

      return state
  },
  arriveTimeStr(state = null, action) {
    const { type, payload } = action
    switch (type) {
      case ACTION_SET_ARRIVE_TIME_STR:
        return payload
      default:
    }

    return state
  },
}

最后來設(shè)置action,action代表的是用戶的操作。redux規(guī)定action一定要包含一個type屬性,且type屬性也要唯一,相同的type,redux視為同一種操作,因為處理action的函數(shù)reducer只判斷action中的type屬性。新建actions.js

export const ACTION_SET_DEPART_DATE = 'SET_DEPART_DATE'
export const ACTION_SET_ARRIVE_DATE = 'SET_ARRIVE_DATE'
export const ACTION_SET_DEPART_TIME_STR = 'SET_DEPART_TIME_STR'
export const ACTION_SET_ARRIVE_TIME_STR = 'SET_ARRIVE_TIME_STR'

export function setDepartDate(departDate) {
  return {
    type: ACTION_SET_DEPART_DATE,
    payload: departDate,
  }
}
export function setArriveDate(arriveDate) {
  return {
    type: ACTION_SET_ARRIVE_DATE,
    payload: arriveDate,
  }
}
export function setDepartTimeStr(departTimeStr) {
  return {
    type: ACTION_SET_DEPART_TIME_STR,
    payload: departTimeStr,
  }
}
export function setArriveTimeStr(arriveTimeStr) {
  return {
    type: ACTION_SET_ARRIVE_TIME_STR,
    payload: arriveTimeStr,
  }
}

// 異步action寫法
export function nextDate() {
  return (dispatch, getState) => {
    const { departDate } = getState()

    dispatch(setDepartDate(h0(departDate) + 86400 * 1000))
  }
}

然后我們在組件中引入

import { bindActionCreators } from 'redux'

import {
  exchangeFromTo,
  showCitySelector,
} from './actions'

function App(props) {
  const cbs = useMemo(() => {
    return bindActionCreators(
      {
        exchangeFromTo,
        showCitySelector,
      }, dispatch)
  }, [])
}

export default connect(
  function mapStateToProps(state) {
    return state
  },
  function mapDispatchToProps(dispatch) {
    return { dispatch }
  }
)(App)

bindActionCreators是redux的一個API,作用是將單個或多個ActionCreator轉(zhuǎn)化為dispatch(action)的函數(shù)集合形式。開發(fā)者不用再手動dispatch(actionCreator(type))

四、React hooks

1. useState

function App(props) {
  const [count, setCount] = useState(() => { // 只運行一次
    return props.defaultCount || 0 // 返回值就是usestate默認值
  })
  return (
    <button
      type="button"
      onClick={() => {setCount(count+1)}}
    >
      Click({count})
    </button>

  );
}

2. useEffect

在函數(shù)組件主體內(nèi)(這里指在 React 渲染階段)改變 DOM、添加訂閱、設(shè)置定時器、記錄日志以及執(zhí)行其他包含副作用的操作都是不被允許的,因為這可能會產(chǎn)生莫名其妙的 bug 并破壞 UI 的一致性。

我們需要使用 useEffect 完成副作用操作。賦值給 useEffect 的函數(shù)會在組件渲染到屏幕之后執(zhí)行。
組件卸載時需要清除 effect 創(chuàng)建的諸如訂閱或計時器 ID 等資源。要實現(xiàn)這一點,useEffect 函數(shù)需返回一個清除函數(shù)

function App(props) {
  const [count, setCount] = useState(() => {
    return props.defaultCount || 0
  })
  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight,
  })
  const onResize = () => {
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight,
    })
  }
  useEffect(() => {
    document.title = count
  })
  useEffect(() => {
    window.addEventListener("resize", onResize, false)
    return () => { // 解綁
      window.removeEventListener("resize", onResize, false)
    }
  }, []) // 數(shù)組中的每一項都不變,useEffect才不會執(zhí)行
  return (
    <button
      type="button"
      onClick={() => {setCount(count+1)}}
    >
      Click({count})
      size: {size.width}X{size.height}
    </button>
  );
}

effect 的執(zhí)行時機
componentDidMountcomponentDidUpdate 不同的是,在瀏覽器完成布局與繪制之后,傳給 useEffect 的函數(shù)會延遲調(diào)用。這使得它適用于許多常見的副作用場景,比如如設(shè)置訂閱和事件處理等情況,因此不應(yīng)在函數(shù)中執(zhí)行阻塞瀏覽器更新屏幕的操作。

然而,并非所有 effect 都可以被延遲執(zhí)行。例如,在瀏覽器執(zhí)行下一次繪制前,用戶可見的 DOM 變更就必須同步執(zhí)行,這樣用戶才不會感覺到視覺上的不一致。(概念上類似于被動監(jiān)聽事件和主動監(jiān)聽事件的區(qū)別。)React 為此提供了一個額外的 useLayoutEffect Hook 來處理這類 effect。它和 useEffect 的結(jié)構(gòu)相同,區(qū)別只是調(diào)用時機不同。

雖然 useEffect 會在瀏覽器繪制后延遲執(zhí)行,但會保證在任何新的渲染前執(zhí)行。React 將在組件更新前刷新上一輪渲染的 effect。

3. useContext

之前context時用法以介紹郭,可翻看上面。

4. useMemo

useMemo(() => fn) === useCallback(fn)

useMemo和useCallback都會在組件第一次渲染的時候執(zhí)行,之后會在其依賴的變量發(fā)生改變時再次執(zhí)行;并且這兩個hooks都返回緩存的值,useMemo返回緩存的變量,useCallback返回緩存的函數(shù)

參考:https://blog.csdn.net/sinat_17775997/article/details/94453167

function Counter(props) {
  return (
    <h1>{props.count}</h1>
  )
}

function App(props) {
  const [count, setCount] = useState(() => {
    return props.defaultCount || 0
  })
  
  const double = useMemo(() => {
    return count*2
  }, [count]) // 空數(shù)組代表只執(zhí)行一次

  return (
    <div>
      <button
      type="button"
      onClick={() => {setCount(count+1)}}
      >
        Click({count}) {double}
      </button>
      <Counter count={count}/>
    </div>
  );
}

5. useReducer

在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較復(fù)雜且包含多個子值,或者下一個 state 依賴于之前的 state 等。并且,使用 useReducer 還能給那些會觸發(fā)深更新的組件做性能優(yōu)化,因為你可以向子組件傳遞 dispatch 而不是回調(diào)函數(shù)

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

惰性初始化(異步),只要把第三個參數(shù)轉(zhuǎn)為一個函數(shù)即可

function init(initialCount) {
  return {count: initialCount};
}
const [state, dispatch] = useReducer(reducer, initialCount, init)

6. useRef

  1. 一個常見的用例便是命令式地訪問子組件:
    useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化為傳入的參數(shù)(initialValue)。返回的 ref 對象在組件的整個生命周期內(nèi)保持不變。
function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已掛載到 DOM 上的文本輸入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
  1. 然而,useRef()ref 屬性更有用。它可以很方便地保存任何可變值,其類似于在 class 中使用實例字段的方式。

這是因為它創(chuàng)建的是一個普通 Javascript 對象。而 useRef() 和自建一個 {current: ...}對象的唯一區(qū)別是,useRef 會在每次渲染時返回同一個 ref 對象。

6. useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以讓你在使用 ref 時自定義暴露給父組件的實例值。在大多數(shù)情況下,應(yīng)當避免使用 ref 這樣的命令式代碼。useImperativeHandle 應(yīng)當與 forwardRef 一起使用:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

在本例中,渲染 <FancyInput ref={fancyInputRef} /> 的父組件可以調(diào)用 fancyInputRef.current.focus()。

7. useLayoutEffect

其函數(shù)簽名與 useEffect 相同,但它會在所有的 DOM 變更之后同步調(diào)用 effect??梢允褂盟鼇碜x取 DOM 布局并同步觸發(fā)重渲染。在瀏覽器執(zhí)行繪制之前,useLayoutEffect 內(nèi)部的更新計劃將被同步刷新。

盡可能使用標準的 useEffect 以避免阻塞視覺更新。

8. hooks使用規(guī)則

不要在循環(huán),條件或嵌套函數(shù)中調(diào)用 Hook
確保總是在你的 React 函數(shù)的最頂層調(diào)用他們。遵守這條規(guī)則,你就能確保 Hook 在每一次渲染中都按照同樣的順序被調(diào)用。這讓 React 能夠在多次的 useStateuseEffect 調(diào)用之間保持 hook 狀態(tài)的正確。
只在 React 函數(shù)中調(diào)用 Hook
不要在普通的 JavaScript 函數(shù)中調(diào)用 Hook。你可以:

  • ? 在 React 的函數(shù)組件中調(diào)用 Hook
  • ? 在自定義 Hook 中調(diào)用其他 Hook
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容