天天看點

React Hooks 你了解嗎?

什麼是React Hook

React Hook是React 16.8版本之後添加的新屬性,用最簡單的話來說,React Hook就是一些React提供的内置函數,這些函數可以讓Function Component和Class Component一樣能夠擁有元件狀态(state)以及進行副作用(side effect)。

常用Hook介紹

接下來我将會為大家介紹一些常用的Hook,對于每一個Hook,我都會覆寫以下方面的内容:

作用

用法

注意事項

useState

作用

useState了解起來非常簡單,和Class Component的this.state一樣,都是用來管理元件狀态的。在React Hook沒出來之前,Function Component也叫做Functional Stateless Component(FSC),這是因為Function Component每次執行的時候都會生成新的函數作用域是以同一個元件的不同渲染(render)之間是不能夠共用狀态的,是以開發者一旦需要在元件中引入狀态就需要将原來的Function Component改成Class Component,這使得開發者的體驗十分不好。useState就是用來解決這個問題的,它允許Function Component将自己的狀态持久化到React運作時(runtime)的某個地方(memory cell),這樣在元件每次重新渲染的時候都可以從這個地方拿到該狀态,而且當該狀态被更新的時候,元件也會重渲染。

用法

const [state, setState] = useState(initialState)

useState接收一個initialState變量作為狀态的初始值,傳回值是一個數組。傳回數組的第一個元素代表目前state的最新值,第二個元素是一個用來更新state的函數。這裡要注意的是state和setState這兩個變量的命名不是固定的,應該根據你業務的實際情況選擇不同的名字,可以是text和setText,也可以是width和setWidth這類的命名。(對上面數組解構指派不熟悉的同學可以看下MDN的介紹)。

我們在實際開發中,一個元件可能不止一個state,如果元件有多個state,則可以在元件内部多次調用useState,以下是一個簡單的例子:

import React, { useState } from 'react'
import ReactDOM from 'react-dom'

const App = () => {
  const [counter, setCounter] = useState(0)
  const [text, setText] = useState('')

  const handleTextChange = (event) => {
    setText(event.target.value)
  }

  return (
    <>
      <div>Current counter: {counter}</div>
      <button
        onClick={() => setCounter(counter + 1)}
      >
        Increase counter
      </button>
      <input
        onChange={handleTextChange}
        value={text}
      />
    </>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
           

和Class Component的this.setState API類似,setCounter和setText都可以接收一個函數為參數,這個函數叫做updater,updater接收的參數是目前狀态的最新值,傳回值是下一個狀态。例如setCounter的參數可以改成一個函數:

<button
  onClick={() => {
    setCounter(counter => counter + 1)
  }}
>
  Increase counter
</button>
           

useState的initialState也可以是一個用來生成狀态初始值的函數,這種做法主要是避免元件每次渲染的時候initialState需要被重複計算。下面是個簡單的例子:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props)
  return initialState
})
           

注意事項

setState是全量替代

Function Component的setState和Class Component的this.setState函數的一個重要差別是this.setState函數是将目前設定的state淺歸并(shallowly merge)到舊state的操作。而setState函數則是将新state直接替換舊的state(replace)。是以我們在編寫Function Component的時候,就要合理劃分state,避免将沒有關系的狀态放在一起管理,例如下面這個是不好的設計:

const [state, setState] = useState({ left: 0, top: 0, width: 0, height: 0 })

在上面代碼中,由于我們将互不關聯的DOM位置資訊{left: 0, top: 0}和大小資訊{width: 0, height: 0}綁定在同一個state,是以我們在更新任意一個狀态的時候也要維護一下另外一個狀态:

const handleContainerResize = ({ width, height }) => {
  setState({...state, width, height})
}

const handleContainerMove = ({ left, top }) => {
  setState({...state, left, top})
}
           

這種寫法十分不友善而且容易引發bug,更加合理的做法應該是将位置資訊和大小資訊放在兩個不同的state裡面,這樣可以避免更新某個狀态的時候要手動維護另一個狀态:

const [position, setPosition] = useState({ left: 0, top: 0 })
const [size, setSize] = useState({ width: 0, height: 0})

const handleContainerResize = ({ width, height }) => {
  setSize({width, height})
}

const handleContainerMove = ({ left, top }) => {
  setPosition({left, top})
}
           

如果你确實要将多個互不關聯的狀态放在一起的話,建議你使用useReducer來管理你的狀态,這樣你的代碼會更好維護。

設定相同的state值時setState會bailing out of update

如果setState接收到的新的state和目前的state是一樣的(判斷方法是Object.is),React将不會重新渲染子元件或者觸發side effect。這裡要注意的是雖然React不會渲染子元件,不過它還是會重新渲染目前的元件的,如果你的元件渲染有些很耗性能的計算的話,可以考慮使用useMemo來優化性能。

setState沒有回調函數

無論是useState還是Class Component的this.setState都是異步調用的,也就是說每次元件調用完它們之後都不能拿到最新的state值。為了解決這個問題,Class Component的this.setState允許你通過一個回調函數來擷取到最新的state值,用法如下:

this.setState(newState, state => {

console.log(“I get new state”, state)

})

而Function Component的setState函數不存在這麼一個可以拿到最新state的回調函數,不過我們可以使用useEffect來實作相同的效果,具體可以參見StackOverflow的這個讨論。

useEffect

作用

useEffect是用來使Function Component也可以進行副作用的。那麼什麼是副作用呢?

通俗來說,函數的副作用就是函數除了傳回值外對外界環境造成的其它影響。舉個例子,假如我們每次執行一個函數,該函數都會操作全局的一個變量,那麼對全局變量的操作就是這個函數的副作用。而在React的世界裡,我們的副作用大體可以分為兩類,一類是調用浏覽器的API,例如使用addEventListener來添加事件監聽函數等,另外一類是發起擷取伺服器資料的請求,例如當使用者卡片挂載的時候去異步擷取使用者的資訊等。在Hook出來之前,如果我們需要在元件中進行副作用的話就需要将元件寫成Class Component,然後在元件的生命周期函數裡面寫副作用,這其實會引起很多代碼設計上的問題,具體大家可以檢視我的上篇文章React為什麼需要Hook。Hook出來之後,開發者就可以在Function Component中使用useEffect來定義副作用了。雖然useEffect基本可以覆寫componentDidMount, componentDidUpdate,componentWillUnmount等生命周期函數組合起來使用的所有場景,但是useEffect和生命周期函數的設計理念還是存在本質上的差別的,如果一味用生命周期函數的思考方式去了解和使用useEffect的話,可能會引發一些奇怪的問題,大家有興趣的話,可以看看React核心開發Dan寫的這篇文章:A Complete Guide to useEffect,裡面闡述了使用useEffect的一個比較正确的思考方式(mental model)。

用法

useEffect(effect, dependencies?)

useEffect的第一個參數effect是要執行的副作用函數,它可以是任意的使用者自定義函數,使用者可以在這個函數裡面操作一些浏覽器的API或者和外部環境進行互動,這個函數會在每次元件渲染完成之後被調用,例如下面是一個簡單的例子:

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'

const UserDetail = ({ userId }) => {
  const [userDetail, setUserDetail] = useState({})

  useEffect(() => {
    fetch(`https://myapi/users/${userId}`)
      .then(response => response.json())
      .then(user => setUserDetail(userDetail))
  })

  return (
    <div>
      <div>User Name: {userDetail.name}</div>
    </div>
  )
}

ReactDOM.render(<UserDetail />, document.getElementById('root'))
           

上面定義的擷取使用者詳情資訊的副作用會在UserDetail元件每次完成渲染後執行,是以當該元件第一次挂載的時候就會向伺服器發起擷取使用者詳情資訊的請求然後更新userDetail的值,這裡的第一次挂載我們可以類比成Class Component的componentDidMount。可是如果試着運作一下上面的代碼的話,你會發現代碼進入了死循環:元件會不斷向服務端發起請求。出現這個死循環的原因是useEffect裡面調用了setUserDetail,這個函數會更新userDetail的值,進而使元件重渲染,而重渲染後useEffect的effect繼續被執行,進而元件再次重渲染。。。為了避免重複的副作用執行,useEffect允許我們通過第二個參數dependencies來限制該副作用什麼時候被執行:指明了dependencies的副作用,隻有在dependencies數組裡面的元素的值發生變化時才會被執行,是以如果要避免上面的代碼進入死循環我們就要将userId指定為我們定義的副作用的dependencies:

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'

const UserDetail = ({ userId }) => {
  const [userDetail, setUserDetail] = useState({})

  useEffect(() => {
    fetch(`https://myapi/users/${userId}`)
      .then(response => response.json())
      .then(user => setUserDetail(userDetail))
  }, [userId])

  return (
    <div>
      <div>User Name: ${userDetail.name}</div>
    </div>
  )
}

ReactDOM.render(<UserDetail />, document.getElementById('root'))
           

除了發起服務端的請求外,我們往往還需要在useEffect裡面調用浏覽器的API,例如使用addEventListener來添加浏覽器事件的監聽函數等。我們一旦使用了addEventListener就必須在合适的時候調用removeEventListener來移除對事件的監聽,否則會有性能問題,useEffect允許我們在副作用函數裡面傳回一個cleanup函數,這個函數會在元件重新渲染之前被執行,我們可以在這個傳回的函數裡面移除對事件的監聽,下面是一個具體的例子:

import React, { useEffect } from 'react'
import ReactDOM from 'react-dom'

const WindowScrollListener = () => {
  useEffect(() => {
    const handleWindowScroll = () => console.log('yean, window is scrolling!')
    window.addEventListener('scroll', handleWindowScroll)

    // this is clean up function
    return () => {
      window.removeEventListener(handleWindowScroll)
    }
  }, [])

  return (
    <div>
      I can listen to the window scroll event!
    </div>
  )
}

ReactDOM.render(<WindowScrollListener />, document.getElementById('root'))
           

上面的代碼中我們會在WindowScrollListener元件首次渲染完成後注冊一個監聽頁面滾動事件的函數,并在元件下一次渲染前移除該監聽函數。由于我們指定了一個空數組作為這個副作用的dependencies,是以這個副作用隻會在元件首次渲染時被執行一次,而它的cleanup函數隻會在元件unmount時才被執行,這就避免了頻繁注冊頁面監聽函數進而影響頁面的性能。

注意事項

避免使用“舊的”變量

我們在實際使用useEffect的過程中可能遇到最多的問題就是我們的effect函數被調用的時候,拿到的某些state, props或者是變量不是最新的變量而是之前舊的變量。出現這個問題的原因是:我們定義的副作用其實就是一個函數,而JS的作用域是詞法作用域,是以函數使用到的變量值是它被定義時就确定的,用最簡單的話來說就是,useEffect的effect會記住它被定義時的外部變量的值,是以它被調用時使用到的值可能不是最新的值。解決這個問題的辦法有兩種,一種是将那些你希望每次effect被調用時拿到的都是最新值的變量儲存在一個ref裡面,并且在每次元件渲染的時候更新該ref的值:

const [someState, setSomeState] = useState()
const someStateRef = useRef()

someStateRef.current = someState

useEffect(() => {
  ...
  const latestSomeState = someStateRef.current
  console.log(latestSomeState)
}, [otherDependencies...])
           

這種做法雖然不是很優雅,不過可以解決我們的問題,如果你沒有了解過useRef用法的話,可以檢視本篇文章useRef這部分内容。解決這個問題的另外一個做法是将副作用使用到的所有變量都加到effect的dependencies中去,這也是比較推薦的做法。在實際開發中我們可以使用facebook自家的eslint-plugin-react-hooks的exhaustive-deps規則來進行編碼限制,在你的項目加上這個限制之後,在代碼開發階段eslint就會告訴你要将someState放到useEffect的dependencies中去,這樣就可以不使用useRef來存儲someState的值了,例如下面代碼:

const [someState, setSomeState] = useState()

useEffect(() => {
  ...
  console.log(someState)
}, [otherDependencies..., someState])
           

useRef

作用

useRef是用來在元件不同渲染之間共用一些資料的,它的作用和我們在Class Component裡面為this指派是一樣的。

用法

const refObject = useRef(initialValue)
           

useRef接收initialValue作為初始值,它的傳回值是一個ref對象,這個對象的.current屬性就是該資料的最新值。使用useRef的一個最簡單的情況就是在Function Component裡面存儲對DOM對象的引用,例如下面這個例子:

import { useRef, useEffect } from 'react'
import ReactDOM from 'react-dom'

const AutoFocusInput = () => {
  const inputRef = useRef(null)

  useEffect(() => {
    // auto focus when component mount
    inputRef.current.focus()
  }, [])

  return (
    <input ref={inputRef} type='text' />
  )
}

ReactDOM.render(<AutoFocusInput />, document.getElementById('root'))
           

在上面代碼中inputRef其實就是一個{current: inputDomInstance}對象,隻不過它可以保證在元件每次渲染的時候拿到的都是同一個對象。

注意事項

更新ref對象不會觸發元件重渲染

useRef傳回的ref object被重新指派的時候不會引起元件的重渲染,如果你有這個需求的話請使用useState來存儲資料。

useCallback

作用

随着Hook的出現,開發者開始越來越多地使用Function Component來開發需求。當開發者在定義Function Component的時候往往需要在函數體内定義一些内嵌函數(inline function),這些内嵌函數會在元件每次重新渲染的時候被重新定義,如果它們作為props傳遞給了子元件的話,即使其它props的值沒有發生變化,它都會使子元件重新渲染,而無用的元件重渲染可能會産生一些性能問題。每次重新生成新的内嵌函數還有另外一個問題就是當我們把内嵌函數作為dependency傳進useEffect的dependencies數組的話,因為該函數頻繁被重新生成,是以useEffect裡面的effect就會頻繁被調用。為了解決上述問題,React允許我們使用useCallback來記住(memoize)目前定義的函數,并在下次元件渲染的時候傳回之前定義的函數而不是使用新定義的函數。

用法

const memoizedCallback = useCallback(callback, dependencies)
           

useCallback接收兩個參數,第一個參數是需要被記住的函數,第二個參數是這個函數的dependencies,隻有dependencies數組裡面的元素的值發生變化時useCallback才會傳回新定義的函數,否則useCallback都會傳回之前定義的函數。下面是一個簡單的使用useCallback來優化子元件頻繁被渲染的例子:

import React, { useCallback } from 'react'
import useSearch from 'hooks/useSearch'
import ReactDOM from 'react-dom'

const HugeList = ({ items, onClick }) => {
  return (
    <div>
      {
        items.map((item, index) => (
          <div
            key={index}
            onClick={() => onClick(index)}
          >
            {item}
          </div>
        ))
      }
    </div>
  )
}

const MemoizedHugeList = React.memo(HugeList)

const SearchApp = ({ searchText }) => {
  const handleClick = useCallback(item => {
    console.log('You clicked', item)
  }, [])
  const items = useSearch(searchText)

  return (
    <MemoizedHugeList
      items={items}
      onClick={handleClick}
    />
  )
}

ReactDOM.render(<SearchApp />, document.getElementById('root'))
           

上面的例子中我定義了一個HugeList元件,由于這個元件需要渲染一個大的清單(items),是以每次重渲染都是十分消耗性能的,是以我使用了React.memo函數來讓該元件隻有在onClick函數和items數組發生變化的時候才被渲染,如果大家對React.memo不是很熟悉的話,可以看看我寫的這篇文章。接着我在SearchApp裡面使用MemoizedHugeList,由于要避免該元件的重複渲染,是以我使用了useCallback來記住定義的handleClick函數,這樣在元件後面渲染的時候,handleClick變量指向的都是同一個函數,是以MemorizedHugeList隻有在items發生變化時才會重新渲染。這裡要注意的是由于我的handleClick函數沒有使用到任何的外部依賴是以它的dependencies才是個空數組,如果你的函數有使用到外面的依賴的話,記得一定要将該依賴放進useCallback的dependencies參數中,不然會有bug發生。

注意事項

避免在函數裡面使用“舊的”變量

和useEffect類似,我們也需要将所有在useCallback的callback中使用到的外部變量寫到dependencies數組裡面,不然我們可能會在callback調用的時候使用到“舊的”外部變量的值。

不是所有函數都要使用useCallback

任何優化都會有代價,useCallback也是一樣的。當我們在Function Component裡面調用useCallback函數的時候,React背後要做一系列計算才能保證當dependencies不發生變化的時候,我們拿到的是同一個函數,是以如果我們濫用useCallback的話,并不會帶來想象中的性能優化,反而會影響到我們的性能,例如下面這個例子就是一個不好的使用useCallback的例子:

import React, { useCallback } from 'react'
import ReactDOM from 'react-dom'

const DummyButton = () => {
  const handleClick = useCallback(() => {
    console.log('button is clicked')
  }, [])

  return (
    <button onClick={handleClick}>
      I'm super dummy
    </button>
  )
}

ReactDOM.render(<DummyButton />, document.getElementById('root'))
           

上面例子使用的useCallback沒有起到任何優化代碼性能的作用,因為上面的代碼執行起來其實相當于下面的代碼:

import React, { useCallback } from 'react'
import ReactDOM from 'react-dom'

const DummyButton = () => {
  const inlineClick = () => {
    console.log('button is clicked')
  }
  const handleClick = useCallback(inlineClick, [])

  return (
    <button onClick={handleClick}>
      I'm super dummy
    </button>
  )
}

ReactDOM.render(<DummyButton />, document.getElementById('root'))
           

從上面的代碼我們可以看出,即使我們使用了useCallback函數,浏覽器在執行DummyButton這個函數的時候還是需要建立一個新的内嵌函數inlineClick,這和不使用useCallback的效果是一樣的,而且除此之外,優化後的代碼由于還調用了useCallback函數,是以它消耗的計算資源其實比沒有優化之前還多,而且由于useCallback函數内部存儲了一些額外的變量(例如之前的dependencies)是以它消耗的記憶體資源也會更多。是以我們并不能一味地将所有的内嵌函數使用useCallback來包裹,隻對那些真正需要被記住的函數使用useCallback。

useMemo

作用

useMemo和useCallback的作用十分類似,隻不過它允許你記住任何類型的變量(不隻是函數)。

用法

const memoizedValue = useMemo(() => valueNeededToBeMemoized, dependencies)
           

useMemo接收一個函數,該函數的傳回值就是需要被記住的變量,當useMemo的第二個參數dependencies數組裡面的元素的值沒有發生變化的時候,memoizedValue使用的就是上一次的值。下面是一個例子:

import React, { useMemo } from 'react'
import ReactDOM from 'react-dom'

const RenderPrimes = ({ iterations, multiplier }) => {
  const primes = React.useMemo(() => calculatePrimes(iterations, multiplier), [
    iterations,
    multiplier
  ])

  return (
    <div>
      Primes! {primes}
    </div>
  )
}

ReactDOM.render(<RenderPrimes />, document.getElementById('root'))
           

上面的例子中calculatePrimes是用來計算素數的,是以每次調用它都需要消耗大量的計算資源。為了提高元件渲染的性能,我們可以使用useMemo來記住計算的結果,當iterations和multiplier保持不變的時候,我們就不需要重新執行calculatePrimes函數來重新計算了,直接使用上一次的結果即可。

注意事項

不是所有的變量要包裹在useMemo裡面

和useCallback類似,我們隻将那些确實有需要被記住的變量使用useMemo來封裝,切記不能濫用useMemo,例如下面就是一個濫用useMemo的例子:

import React, { useMemo } from 'react'
import ReactDOM from 'react-dom'

const DummyDisplay = () => {
  const items = useMemo(() => ['1', '2', '3'], [])
  
  return (
    <>
      {
        items.map(item => <div key={item}>{item}</div>)
      }
    </>
  )
}

ReactDOM.render(<DummyDisplay />, document.getElementById('root'))
           

上面的例子中直接将items定義在元件外面會更好:

import React from 'react'
import ReactDOM from 'react-dom'

const items = ['1', '2', '3']

const DummyDisplay = () => {  
  return (
    <>
      {
        items.map(item => <div key={item}>{item}</div>)
      }
    </>
  )
}

ReactDOM.render(<DummyDisplay />, document.getElementById('root'))
           

useContext

作用

我們知道React中元件之間傳遞參數的方式是props,假如我們在父級元件中定義了某些狀态,而這些狀态需要在該元件深層次嵌套的子元件中被使用的話就需要将這些狀态以props的形式層層傳遞,這就造成了props drilling的問題。為了解決這個問題,React允許我們使用Context來在父級元件和底下任意層次的子元件之間傳遞狀态。在Function Component中我們可以使用useContext Hook來使用context。

用法

const value = useContext(MyContext)
           

useContext接收一個context對象為參數,該context對象是由React.createContext函數生成的。useContext的傳回值是目前context的值,這個值是由最鄰近的<MyContext.Provider>來決定的。一旦在某個元件裡面使用了useContext這就相當于該元件訂閱了這個context的變化,當最近的<MyContext.Provider>的context值發生變化時,使用到該context的子元件就會被觸發重渲染,且它們會拿到context的最新值。下面是一個具體的例子:

import React, { useContext, useState } from 'react'
import ReactDOM from 'react-dom'

const NumberContext = React.createContext()

const NumberDisplay = () => {
  const [currentNumber, setCurrentNumber] = useContext(NumberContext)

  const handleCurrentNumberChange = () => {
    setCurrentNumber(Math.floor(Math.random() * 100))
  }

  return (
    <>
      <div>Current number is: {currentNumber}</div>
      <button onClick={handleCurrentNumberChange}>Change current number</button>
    </>
  )
}

const ParentComponent = () => {
  const [currentNumber, setCurrentNumber] = useState({})

  return (
    <NumberContext.Provider value={[currentNumber, setCurrentNumber]}>
      <NumberDisplay />
    </NumberContext.Provider>
  )
}

ReactDOM.render(<ParentComponent />, document.getElementById('root'))
           

注意事項

避免無用渲染

我們在上面已經提到如果一個Function Component使用了useContext(SomeContext)的話它就訂閱了這個SomeContext的變化,這樣當SomeContext.Provider的value發生變化的時候,這個元件就會被重新渲染。這裡有一個問題就是,我們可能會把很多不同的資料放在同一個context裡面,而不同的子元件可能隻關心這個context的某一部分資料,當context裡面的任意值發生變化的時候,無論這些元件用不用到這些資料它們都會被重新渲染,這可能會造成一些性能問題。下面是一個簡單的例子:

import React, { useContext, useState } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'

const AppContext = React.createContext()

const ChildrenComponent = () => {
  const [appContext] = useContext(AppContext)
  const theme = appContext.theme

  return (
    <div>
      <ExpensiveTree theme={theme} />
    </div>
  )
}

const App = () => {
  const [appContext, setAppContext] = useState({ theme: { color: 'red' }, configuration: { showTips: false }})

  return (
    <AppContext.Provider value={[appContext, setAppContext]}>
      <ChildrenComponent />
    </AppContext.Provider>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
           

在上面的例子中,ChildrenComponent隻使用到了appContext的.theme屬性,可是當appContext其它屬性例如configuration被更新時,ChildrenComponent也會被重新渲染,而ChildrenComponent調用了一個十分耗費性能的ExpensiveTree元件,是以這些無用的渲染會影響到我們頁面的性能,解決上面這個問題的方法有下面三種:

拆分Context

這個方法是最被推薦的做法,和useState一樣,我們可以将不需要同時改變的context拆分成不同的context,讓它們的職責更加分明,這樣子元件隻會訂閱那些它們需要訂閱的context進而避免無用的重渲染。例如上面的代碼可以改成這樣:

import React, { useContext, useState } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'

const ThemeContext = React.createContext()
const ConfigurationContext = React.createContext()

const ChildrenComponent = () => {
  const [themeContext] = useContext(ThemeContext)

  return (
    <div>
      <ExpensiveTree theme={themeContext} />
    </div>
  )
}

const App = () => {
  const [themeContext, setThemeContext] = useState({ color: 'red' })
  const [configurationContext, setConfigurationContext] = useState({ showTips: false })

  return (
    <ThemeContext.Provider value={[themeContext, setThemeContext]}>
      <ConfigurationContext.Provider value={[configurationContext, setConfigurationContext]}>
        <ChildrenComponent />
      </ConfigurationContext.Provider>
    </ThemeContext.Provider>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
           

拆分你的元件,使用memo來優化消耗性能的元件

如果出于某些原因你不能拆分context,你仍然可以通過将消耗性能的元件和父元件的其他部分分離開來,并且使用memo函數來優化消耗性能的元件。例如上面的代碼可以改為:

import React, { useContext, useState } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'

const AppContext = React.createContext()

const ExpensiveComponentWrapper = React.memo(({ theme }) => {
  return (
    <ExpensiveTree theme={theme} />
  )
})

const ChildrenComponent = () => {
  const [appContext] = useContext(AppContext)
  const theme = appContext.theme

  return (
    <div>
      <ExpensiveComponentWrapper theme={theme} />
    </div>
  )
}

const App = () => {
  const [appContext, setAppContext] = useState({ theme: { color: 'red' }, configuration: { showTips: false }})

  return (
    <AppContext.Provider value={[appContext, setAppContext]}>
      <ChildrenComponent />
    </AppContext.Provider>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
           

不拆分元件,也可以使用useMemo來優化

當然我們也可以不拆分元件使用useMemo來将上面的代碼進行優化,代碼如下:

import React, { useContext, useState, useMemo } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'

const AppContext = React.createContext()

const ChildrenComponent = () => {
  const [appContext] = useContext(AppContext)
  const theme = appContext.theme

  return useMemo(() => (
      <div>
        <ExpensiveTree theme={theme} />
      </div>
    ),
    [theme]
  )
}

const App = () => {
  const [appContext, setAppContext] = useState({ theme: { color: 'red' }, configuration: { showTips: false }})

  return (
    <AppContext.Provider value={[appContext, setAppContext]}>
      <ChildrenComponent />
    </AppContext.Provider>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
           

useReducer

作用

useReducer用最簡單的話來說就是允許我們在Function Component裡面像使用redux一樣通過reducer和action來管理我們元件狀态的變換(state transition)。

用法

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

useReducer和useState類似,都是用來管理元件狀态的,隻不過和useState的setState不一樣的是,useReducer傳回的dispatch函數是用來觸發某些改變state的action而不是直接設定state的值,至于不同的action如何産生新的state的值則在reducer裡面定義。useReducer接收的三個參數分别是:

reducer: 這是一個函數,它的簽名是(currentState, action) => newState,從它的函數簽名可以看出它會接收目前的state和目前dispatch的action為參數,然後傳回下一個state,也就是說它負責狀态轉換(state transition)的工作。

initialArg:如果調用者沒有提供第三個init參數,這個參數代表的是這個reducer的初始狀态,如果init參數有被指定的話,initialArg會被作為參數傳進init函數來生成初始狀态。

init: 這是一個用來生成初始狀态的函數,它的函數簽名是(initialArg) => initialState,從它的函數簽名可以看出它會接收useReducer的第二個參數initialArg作為參數,并生成一個初始狀态initialState。

下面是useReducer的一個簡單的例子:

import React, { useState, useReducer } from 'react'

let todoId = 1

const reducer = (currentState, action) => {
  switch(action.type) {
    case 'add':
      return [...currentState, {id: todoId++, text: action.text}]
    case 'delete':
      return currentState.filter(({ id }) => action.id !== id)
    default:
      throw new Error('Unsupported action type')
  }
}

const Todo = ({ id, text, onDelete }) => {
  return (
    <div>
      {text}
      <button
        onClick={() => onDelete(id)}
      >
        remove
      </button>
    </div>
  )
}

const App = () => {
  const [todos, dispatch] = useReducer(reducer, [])
  const [text, setText] = useState('')

  return (
    <>
      {
        todos.map(({ id, text }) => {
          return (
            <Todo
              text={text}
              key={id}
              id={id}
              onDelete={id => {
                dispatch({ type: 'delete', id })
              }}
            />
          )
        })
      }
      <input onChange={event => setText(event.target.value)} />
      <button
        onClick={() => {
          dispatch({ type: 'add', text })
          setText('')
        }}
      >
        add todo
      </button>
    </>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
           

注意事項

useReducer vs useState

useReducer和useState都可以用來管理元件的狀态,它們之間最大的差別就是,useReducer将狀态和狀态的變化統一管理在reducer函數裡面,這樣對于一些複雜的狀态管理會十分友善我們debug,因為它對狀态的改變是封閉的。而由于useState傳回的setState可以直接在任意地方設定我們狀态的值,當我們元件的狀态轉換邏輯十分複雜時,它将很難debug,因為它是開放的狀态管理。總體的來說,在useReducer和useState如何進行選擇的問題上我們可以參考以下這些原則:

下列情況使用useState

state的值是JS原始資料類型(primitives),如number, string和boolean等

state的轉換邏輯十分簡單

元件内不同的狀态是沒有關聯的,它們可以使用多個獨立的useState來單獨管理

下列情況使用useReducer

state的值是object或者array

state的轉換邏輯十分複雜, 需要使用reducer函數來統一管理

元件内多個state互相關聯,改變一個狀态時也需要改變另外一個,将他們放在同一個state内使用reducer來統一管理

狀态定義在父級元件,不過需要在深層次嵌套的子元件中使用和改變父元件的狀态,可以同時使用useReducer和useContext兩個hook,将dispatch方法放進context裡面來避免元件的props drilling

如果你希望你的狀态管理是可預測的(predictable)和可維護的(maintainable),請useReducer

如果你希望你的狀态變化可以被測試,請使用useReducer

自定義Hook

上面介紹了React内置的常用Hook的用法,接着我們看一下如何編寫我們自己的Hook。

作用

自定義Hook的目的是讓我們封裝一些可以在不同元件之間共用的非UI邏輯來提高我們開發業務代碼的效率。

什麼是自定義Hook

之前我們說過Hook其實就是一個函數,是以自定義Hook也是一個函數,隻不過它在内部使用了React的内置Hook或者其它的自定義Hook。雖然我們可以任意命名我們的自定義Hook,可是為了另其它開發者更容易了解我們的代碼以及友善一些開發工具例如eslint-plugin-react-hooks來給我們更好地提示,我們需要将我們的Hook以use作為開頭,并且使用駝峰發進行命名,例如useLocation,useLocalStorage和useQueryString等等。

例子

下面舉一個最簡單的自定義hook的例子:

import React, { useState, useCallback } from 'react'
import ReactDOM from 'react-dom'

const useCounter = () => {
  const [counter, setCounter] = useState(0)
  
  const increase = useCallback(() => setCounter(counter => ++counter), [])
  const decrease = useCallback(() => setCounter(counter => --counter), [])

  return {
    counter,
    increase,
    decrease
  }
}

const App = () => {
  const { counter, increase, decrease } = useCounter()

  return (
    <>
      <div>Counter: {counter}</div>
      <button onClick={increase}>increase</button>
      <button onClick={decrease}>decrease</button>
    </>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))