Taro Hooks

2021-09-23 21:09 更新

v1.3.0-beta-0 起支持

Hooks 是一套全新的 API,可以讓你在不編寫類,不使用 state 的情況下使用 Class 的狀態(tài)管理,生命周期等功能。

關(guān)于 Hooks 的概述、動機和規(guī)則,我們強烈建議你閱讀 React 的官方文檔。和其它大部分 React 特性不同,Hooks 沒有 RFC 介紹,相反,所有說明都在文檔中:

本篇文檔只會介紹在 Taro 中可用的 Hooks API 和部分與 React 不一致的行為,其它內(nèi)容大體的內(nèi)容和 Hooks Reference 相同。

你還可以參考這兩個使用 Hooks 的 Demo:

  • V2EX,主要展示與服務(wù)器通信

  • TodoMVC,主要展示組件間通信

API

在 Taro 中使用 Hooks API 很簡單,Taro 的專有 Hooks(例如 usePageScroll, useReachBottom)從 @tarojs/taro 中引入,框架自己的 Hooks (例如 useEffect, useState)從對應(yīng)的框架引入。

  1. import { usePageScroll, useReachBottom } from '@tarojs/taro' // Taro 專有 Hooks
  2. import { useState, useEffect } from 'react' // 框架 Hooks (基礎(chǔ) Hooks)
  3. // 如果你使用 Nerv 的話
  4. // import { useState, useEffect } from 'nervjs' // 框架 Hooks (基礎(chǔ) Hooks)

useState

  1. const [state, setState] = useState(initialState);

返回一個 state,以及更新 state 的函數(shù)。

在初始渲染期間,返回的狀態(tài) (state) 與傳入的第一個參數(shù) (initialState) 值相同。

setState 函數(shù)用于更新 state。它接收一個新的 state 值并將組件的一次重新渲染加入隊列。

  1. setState(newState);

在后續(xù)的重新渲染中,useState 返回的第一個值將始終是更新后最新的 state。

注意

Taro 會確保 setState 函數(shù)的標識是穩(wěn)定的,并且不會在組件重新渲染時發(fā)生變化。這就是為什么可以安全地從 useEffectuseCallback 的依賴列表中省略 setState。

函數(shù)式更新

如果新的 state 需要通過使用先前的 state 計算得出,那么可以將函數(shù)傳遞給 setState。該函數(shù)將接收先前的 state,并返回一個更新后的值。下面的計數(shù)器組件示例展示了 setState 的兩種用法:

  1. function Counter({initialCount}) {
  2. const [count, setCount] = useState(initialCount);
  3. return (
  4. <View>
  5. Count: {count}
  6. <Button onClick={() => setCount(initialCount)}>Reset</Button>
  7. <Button onClick={() => setCount(prevCount => prevCount + 1)}>+</Button>
  8. <Button onClick={() => setCount(prevCount => prevCount - 1)}>-</Button>
  9. </View>
  10. );
  11. }

“+” 和 “-” 按鈕采用函數(shù)式形式,因為被更新的 state 需要基于之前的 state。但是“重置”按鈕則采用普通形式,因為它總是把 count 設(shè)置回初始值。

注意

與 class 組件中的 setState 方法不同,useState 不會自動合并更新對象。你可以用函數(shù)式的 setState 結(jié)合展開運算符來達到合并更新對象的效果。

  1. setState(prevState => {
  2. // 也可以使用 Object.assign
  3. return {...prevState, ...updatedValues};
  4. });

useReducer 是另一種可選方案,它更適合用于管理包含多個子值的 state 對象。

惰性初始 state

initialState 參數(shù)只會在組件的初始渲染中起作用,后續(xù)渲染時會被忽略。如果初始 state 需要通過復(fù)雜計算獲得,則可以傳入一個函數(shù),在函數(shù)中計算并返回初始的 state,此函數(shù)只在初始渲染時被調(diào)用:

  1. const [state, setState] = useState(() => {
  2. const initialState = someExpensiveComputation(props);
  3. return initialState;
  4. });

useEffect

  1. useEffect(didUpdate);

該 Hook 接收一個包含命令式、且可能有副作用代碼的函數(shù)。

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

使用 useEffect 完成副作用操作。賦值給 useEffect 的函數(shù)會在組件渲染到屏幕之后執(zhí)行。

默認情況下,effect 將在每輪渲染結(jié)束后執(zhí)行,但你可以選擇讓它在只有某些值改變的時候才執(zhí)行。

清除 effect

通常,組件卸載時需要清除 effect 創(chuàng)建的諸如訂閱或計時器 ID 等資源。要實現(xiàn)這一點,useEffect 函數(shù)需返回一個清除函數(shù)。以下就是一個創(chuàng)建訂閱的例子:

  1. useEffect(() => {
  2. const subscription = props.source.subscribe();
  3. return () => {
  4. // 清除訂閱
  5. subscription.unsubscribe();
  6. };
  7. });

為防止內(nèi)存泄漏,清除函數(shù)會在組件卸載前執(zhí)行。另外,如果組件多次渲染(通常如此),則在執(zhí)行下一個 effect 之前,上一個 effect 就已被清除。在上述示例中,意味著組件的每一次更新都會創(chuàng)建新的訂閱。若想避免每次更新都觸發(fā) effect 的執(zhí)行,請參閱下一小節(jié)。

effect 的執(zhí)行時機

componentDidMount、componentDidUpdate 不同的是,Taro 會在 setData 完成之后的下一個 macrotask 執(zhí)行 effect 的回調(diào)函數(shù),傳給 useEffect 的函數(shù)會延遲調(diào)用。這使得它適用于許多常見的副作用場景,比如如設(shè)置訂閱和事件處理等情況,因此不應(yīng)在函數(shù)中執(zhí)行渲染和更新。

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

effect 的條件執(zhí)行

默認情況下,effect 會在每輪組件渲染完成后執(zhí)行。這樣的話,一旦 effect 的依賴發(fā)生變化,它就會被重新創(chuàng)建。

然而,在某些場景下這么做可能會矯枉過正。比如,在上一章節(jié)的訂閱示例中,我們不需要在每次組件更新時都創(chuàng)建新的訂閱,而是僅需要在 source props 改變時重新創(chuàng)建。

要實現(xiàn)這一點,可以給 useEffect 傳遞第二個參數(shù),它是 effect 所依賴的值數(shù)組。更新后的示例如下:

  1. useEffect(
  2. () => {
  3. const subscription = props.source.subscribe();
  4. return () => {
  5. subscription.unsubscribe();
  6. };
  7. },
  8. [props.source],
  9. );

此時,只有當(dāng) props.source 改變后才會重新創(chuàng)建訂閱。

注意

如果你要使用此優(yōu)化方式,請確保數(shù)組中包含了所有外部作用域中會發(fā)生變化且在 effect 中使用的變量,否則你的代碼會引用到先前渲染中的舊變量。

如果想執(zhí)行只運行一次的 effect(僅在組件掛載和卸載時執(zhí)行),可以傳遞一個空數(shù)組([])作為第二個參數(shù)。這就告訴 Taro 你的 effect 不依賴于 props 或 state 中的任何值,所以它永遠都不需要重復(fù)執(zhí)行。這并不屬于特殊情況 —— 它依然遵循輸入數(shù)組的工作方式。

如果你傳入了一個空數(shù)組([]),effect 內(nèi)部的 props 和 state 就會一直擁有其初始值。盡管傳入 [] 作為第二個參數(shù)有點類似于 componentDidMountcomponentWillUnmount 的思維模式,但我們有 更好的 方式來避免過于頻繁的重復(fù)調(diào)用 effect。除此之外,請記得 Taro 會等待渲染完畢之后才會延遲調(diào)用 useEffect,因此會使得額外操作很方便。

Taro 會在自帶的 ESLint 中配置 eslint-plugin-react-hooks 中的 exhaustive-deps 規(guī)則。此規(guī)則會在添加錯誤依賴時發(fā)出警告并給出修復(fù)建議。

useReducer {#usereducer}

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

useState 的替代方案。它接收一個形如 (state, action) => newState 的 reducer,并返回當(dāng)前的 state 以及與其配套的 dispatch 方法。(如果你熟悉 Redux 的話,就已經(jīng)知道它如何工作了。)

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

以下是用 reducer 重寫 useState 一節(jié)的計數(shù)器示例:

  1. const initialState = {count: 0};
  2. function reducer(state, action) {
  3. switch (action.type) {
  4. case 'increment':
  5. return {count: state.count + 1};
  6. case 'decrement':
  7. return {count: state.count - 1};
  8. default:
  9. throw new Error();
  10. }
  11. }
  12. function Counter({initialState}) {
  13. const [state, dispatch] = useReducer(reducer, initialState);
  14. return (
  15. <View>
  16. Count: {state.count}
  17. <Button onClick={() => dispatch({type: 'increment'})}>+</Button>
  18. <Button onClick={() => dispatch({type: 'decrement'})}>-</Button>
  19. </View>
  20. );
  21. }

注意

Taro 會確保 dispatch 函數(shù)的標識是穩(wěn)定的,并且不會在組件重新渲染時改變。這就是為什么可以安全地從 useEffectuseCallback 的依賴列表中省略 dispatch。

指定初始 state

有兩種不同初始化 useReducer state 的方式,你可以根據(jù)使用場景選擇其中的一種。將初始 state 作為第二個參數(shù)傳入 useReducer 是最簡單的方法:

  1. const [state, dispatch] = useReducer(
  2. reducer,
  3. {count: initialCount}
  4. );

注意

Taro 不使用 state = initialState 這一由 Redux 推廣開來的參數(shù)約定。有時候初始值依賴于 props,因此需要在調(diào)用 Hook 時指定。如果你特別喜歡上述的參數(shù)約定,可以通過調(diào)用 useReducer(reducer, undefined, reducer) 來模擬 Redux 的行為,但我們不鼓勵你這么做。

惰性初始化

你可以選擇惰性地創(chuàng)建初始 state。為此,需要將 init 函數(shù)作為 useReducer 的第三個參數(shù)傳入,這樣初始 state 將被設(shè)置為 init(initialArg)

這么做可以將用于計算 state 的邏輯提取到 reducer 外部,這也為將來對重置 state 的 action 做處理提供了便利:

  1. function init(initialCount) {
  2. return {count: initialCount};
  3. }
  4. function reducer(state, action) {
  5. switch (action.type) {
  6. case 'increment':
  7. return {count: state.count + 1};
  8. case 'decrement':
  9. return {count: state.count - 1};
  10. case 'reset':
  11. return init(action.payload);
  12. default:
  13. throw new Error();
  14. }
  15. }
  16. function Counter({initialCount}) {
  17. const [state, dispatch] = useReducer(reducer, initialCount, init);
  18. return (
  19. <View>
  20. Count: {state.count}
  21. <Button
  22. onClick={() => dispatch({type: 'reset', payload: initialCount})}>
  23. Reset
  24. </Button>
  25. <Button onClick={() => dispatch({type: 'increment'})}>+</Button>
  26. <Button onClick={() => dispatch({type: 'decrement'})}>-</Button>
  27. </View>
  28. );
  29. }

useCallback

  1. const memoizedCallback = useCallback(
  2. () => {
  3. doSomething(a, b);
  4. },
  5. [a, b],
  6. );

返回一個 memoized 回調(diào)函數(shù)。

把內(nèi)聯(lián)回調(diào)函數(shù)及依賴項數(shù)組作為參數(shù)傳入 useCallback,它將返回該回調(diào)函數(shù)的 memoized 版本,該回調(diào)函數(shù)僅在某個依賴項改變時才會更新。當(dāng)你把回調(diào)函數(shù)傳遞給經(jīng)過優(yōu)化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子組件時,它將非常有用。

useCallback(fn, deps) 相當(dāng)于 useMemo(() => fn, deps)。

useMemo {#usememo}

  1. const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

返回一個 memoized 值。

把“創(chuàng)建”函數(shù)和依賴項數(shù)組作為參數(shù)傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。這種優(yōu)化有助于避免在每次渲染時都進行高開銷的計算。

記住,傳入 useMemo 的函數(shù)會在渲染期間執(zhí)行。請不要在這個函數(shù)內(nèi)部執(zhí)行與渲染無關(guān)的操作,諸如副作用這類的操作屬于 useEffect 的適用范疇,而不是 useMemo。

如果沒有提供依賴項數(shù)組,useMemo 在每次渲染時都會計算新的值。

useRef

  1. const refContainer = useRef(initialValue);

useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化為傳入的參數(shù)(initialValue)。返回的 ref 對象在組件的整個生命周期內(nèi)保持不變。

一個常見的用例便是命令式地訪問子組件:

  1. function TextInputWithFocusButton() {
  2. const inputEl = useRef(null);
  3. const onButtonClick = () => {
  4. // `current` 指向已掛載到 DOM 上的文本輸入元素
  5. inputEl.current.focus();
  6. };
  7. return (
  8. <View>
  9. <Input ref={inputEl} type="text" />
  10. <Button onClick={onButtonClick}>Focus the input</Button>
  11. </View>
  12. );
  13. }

本質(zhì)上,useRef 就像是可以在其 .current 屬性中保存一個可變值的“盒子”。

你應(yīng)該熟悉 ref 這一種訪問 DOM 的主要方式。如果你將 ref 對象以 <View ref={myRef} /> Taro 都會將 ref 對象的 .current 屬性設(shè)置為相應(yīng)的 DOM 節(jié)點。

然而,useRef()ref 屬性更有用。它可以很方便地保存任何可變值,其類似于在 class 中使用實例字段的方式。

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

請記住,當(dāng) ref 對象內(nèi)容發(fā)生變化時,useRef不會通知你。變更 .current 屬性不會引發(fā)組件重新渲染。如果想要在 Taro 綁定或解綁 DOM 節(jié)點的 ref 時運行某些代碼,則需要使用回調(diào) ref 來實現(xiàn)。

useLayoutEffect

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

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

提示

如果你正在將代碼從 class 組件遷移到使用 Hook 的函數(shù)組件,則需要注意 useLayoutEffectcomponentDidMountcomponentDidUpdate 的調(diào)用階段是一樣的。但是,我們推薦你一開始先用 useEffect,只有當(dāng)它出問題的時再嘗試使用 useLayoutEffect。

useContext

  1. const value = useContext(MyContext)

接收一個 context (Taro.createContext 的返回值)并返回該 context 的當(dāng)前值。當(dāng)前的 context 值由上層組件中最先渲染的 <MyContext.Provider value={value}>value決定。

當(dāng)組件上層最近的 <MyContext.Provider> 更新時,該 Hook 會觸發(fā)重渲染,并使用最新傳遞給 MyContext provider 的 context value 值。

別忘記 useContext 的參數(shù)必須是 context 對象本身:

正確: useContext(MyContext) 錯誤: useContext(MyContext.Consumer) 錯誤: useContext(MyContext.Provider) 調(diào)用了 useContext 的組件總會在 context 值變化時重新渲染。

如果你在接觸 Hook 前已經(jīng)對 context API 比較熟悉,那應(yīng)該可以理解,useContext(MyContext) 相當(dāng)于 class 組件中的 static contextType = MyContext 或者 <MyContext.Consumer>useContext(MyContext) 只是讓你能夠讀取 context 的值以及訂閱 context 的變化。你仍然需要在上層組件樹中使用 <MyContext.Provider> 來為下層組件提供 context。

useDidShow

1.3.14 開始支持

  1. useDidShow(() => {
  2. console.log('componentDidShow')
  3. })

useDidShow 是 Taro 專有的 Hook,等同于 componentDidShow 頁面生命周期鉤子

useDidHide

1.3.14 開始支持

  1. useDidHide(() => {
  2. console.log('componentDidHide')
  3. })

useDidHide 是 Taro 專有的 Hook,等同于 componentDidHide 頁面生命周期鉤子

usePullDownRefresh

1.3.14 開始支持

  1. usePullDownRefresh(() => {
  2. console.log('onPullDownRefresh')
  3. })

usePullDownRefresh 是 Taro 專有的 Hook,等同于 onPullDownRefresh 頁面生命周期鉤子

useReachBottom

1.3.14 開始支持

  1. useReachBottom(() => {
  2. console.log('onReachBottom')
  3. })

useReachBottom 是 Taro 專有的 Hook,等同于 onReachBottom 頁面生命周期鉤子

usePageScroll

1.3.14 開始支持

  1. usePageScroll(res => {
  2. console.log(res.scrollTop)
  3. })

usePageScroll 是 Taro 專有的 Hook,等同于 onPageScroll 頁面生命周期鉤子

useResize

1.3.14 開始支

  1. useResize(res => {
  2. console.log(res.size.windowWidth)
  3. console.log(res.size.windowHeight)
  4. })

useResize 是 Taro 專有的 Hook,等同于 onResize 頁面生命周期鉤子

useShareAppMessage

1.3.14 開始支持

  1. useShareAppMessage(res => {
  2. if (res.from === 'button') {
  3. // 來自頁面內(nèi)轉(zhuǎn)發(fā)按鈕
  4. console.log(res.target)
  5. }
  6. return {
  7. title: '自定義轉(zhuǎn)發(fā)標題',
  8. path: '/page/user?id=123'
  9. }
  10. })

useShareAppMessage 是 Taro 專有的 Hook,等同于 onShareAppMessage 頁面生命周期鉤子

useTabItemTap

1.3.14 開始支持

  1. useTabItemTap(item => {
  2. console.log(item.index)
  3. console.log(item.pagePath)
  4. console.log(item.text)
  5. })

useTabItemTap 是 Taro 專有的 Hook,等同于 onTabItemTap 頁面生命周期鉤子

useRouter

1.3.14 開始支持

  1. const router = useRouter() // { path: '', params: { ... } }

useRouter 是 Taro 專有的 Hook,等同于頁面為類時的 getCurrentInstance().router

useReady

  1. useReady(() => {
  2. const query = wx.createSelectorQuery()
  3. })

useReady 是 Taro 專有的 Hook,等同于頁面的 onReady 生命周期鉤子。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號