Taro Hooks

2021-09-23 21:09 更新

v1.3.0-beta-0 起支持

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

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

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

你還可以參考這兩個(gè)使用 Hooks 的 Demo:

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

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

API

在 Taro 中使用 Hooks API 很簡(jiǎn)單,Taro 的專有 Hooks(例如 usePageScroll, useReachBottom)從 @tarojs/taro 中引入,框架自己的 Hooks (例如 useEffect, useState)從對(duì)應(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);

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

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

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

  1. setState(newState);

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

注意

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

函數(shù)式更新

如果新的 state 需要通過使用先前的 state 計(jì)算得出,那么可以將函數(shù)傳遞給 setState。該函數(shù)將接收先前的 state,并返回一個(gè)更新后的值。下面的計(jì)數(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ù)式形式,因?yàn)楸桓碌?state 需要基于之前的 state。但是“重置”按鈕則采用普通形式,因?yàn)樗偸前?count 設(shè)置回初始值。

注意

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

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

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

惰性初始 state

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

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

useEffect

  1. useEffect(didUpdate);

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

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

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

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

清除 effect

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

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

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

effect 的執(zhí)行時(shí)機(jī)

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

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

effect 的條件執(zhí)行

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

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

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

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

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

注意

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

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

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

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

useReducer {#usereducer}

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

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

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

以下是用 reducer 重寫 useState 一節(jié)的計(jì)數(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 會(huì)確保 dispatch 函數(shù)的標(biāo)識(shí)是穩(wěn)定的,并且不會(huì)在組件重新渲染時(shí)改變。這就是為什么可以安全地從 useEffectuseCallback 的依賴列表中省略 dispatch。

指定初始 state

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

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

注意

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

惰性初始化

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

這么做可以將用于計(jì)算 state 的邏輯提取到 reducer 外部,這也為將來對(duì)重置 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. );

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

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

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

useMemo {#usememo}

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

返回一個(gè) memoized 值。

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

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

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

useRef

  1. const refContainer = useRef(initialValue);

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

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

  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 屬性中保存一個(gè)可變值的“盒子”。

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

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

這是因?yàn)樗鼊?chuàng)建的是一個(gè)普通 JavaScript 對(duì)象。而 useRef() 和自建一個(gè) {current: ...} 對(duì)象的唯一區(qū)別是,useRef 會(huì)在每次渲染時(shí)返回同一個(gè) ref 對(duì)象。

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

useLayoutEffect

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

盡可能使用標(biāo)準(zhǔn)的 useEffect 以避免阻塞視覺更新。

提示

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

useContext

  1. const value = useContext(MyContext)

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

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

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

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

如果你在接觸 Hook 前已經(jīng)對(duì) 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 頁(yè)面生命周期鉤子

useDidHide

1.3.14 開始支持

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

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

usePullDownRefresh

1.3.14 開始支持

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

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

useReachBottom

1.3.14 開始支持

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

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

usePageScroll

1.3.14 開始支持

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

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

useResize

1.3.14 開始支

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

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

useShareAppMessage

1.3.14 開始支持

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

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

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 頁(yè)面生命周期鉤子

useRouter

1.3.14 開始支持

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

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

useReady

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

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


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)