超詳細React Hook實踐指南

React爲何須要Hook這篇文章中咱們探討了React開發團隊爲何要爲Function Component添加Hook的緣由,在本篇文章中我將會爲你們提供一份較爲全面的React Hook實踐指南,其中包括如下方面的內容:javascript

  • 什麼是React Hook
  • 經常使用Hook介紹
    • useState
    • useEffect
    • useRef
    • useCallback
    • useMemo
    • useContext
    • useReducer
  • 自定義Hook

什麼是React Hook

React Hook是React 16.8版本以後添加的新屬性,用最簡單的話來講,React Hook就是一些React提供的內置函數,這些函數可讓Function Component和Class Component同樣可以擁有組件狀態(state)以及進行反作用(side effect)html

經常使用Hook介紹

接下來我將會爲你們介紹一些經常使用的Hook,對於每個Hook,我都會覆蓋如下方面的內容:java

  • 做用
  • 用法
  • 注意事項

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),這樣在組件每次從新渲染的時候均可以從這個地方拿到該狀態,並且當該狀態被更新的時候,組件也會重渲染react

用法

const [state, setState] = useState(initialState)
複製代碼

useState接收一個initialState變量做爲狀態的初始值,返回值是一個數組。返回數組的第一個元素表明當前state的最新值,第二個元素是一個用來更新state的函數。這裏要注意的是statesetState這兩個變量的命名不是固定的,應該根據你業務的實際狀況選擇不一樣的名字,能夠是textsetText,也能夠是widthsetWidth這類的命名。(對上面數組解構賦值不熟悉的同窗能夠看下MDN的介紹)。ios

咱們在實際開發中,一個組件可能不止一個state,若是組件有多個state,則能夠在組件內部屢次調用useState,如下是一個簡單的例子:git

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相似,setCountersetText均可以接收一個函數爲參數,這個函數叫作updaterupdater接收的參數是當前狀態的最新值,返回值是下一個狀態。例如setCounter的參數能夠改爲一個函數:github

<button
  onClick={() => {
    setCounter(counter => counter + 1)
  }}
>
  Increase counter
</button>
複製代碼

useStateinitialState也能夠是一個用來生成狀態初始值的函數,這種作法主要是避免組件每次渲染的時候initialState須要被重複計算。下面是個簡單的例子:express

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,避免將沒有關係的狀態放在一塊兒管理,例以下面這個是很差的設計:npm

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

在上面代碼中,因爲咱們將互不關聯的DOM位置信息{left: 0, top: 0}和大小信息{width: 0, height: 0}綁定在同一個state,因此咱們在更新任意一個狀態的時候也要維護一下另一個狀態:json

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

const handleContainerMove = ({ left, top }) => {
  setState({...state, left, top})
}
複製代碼

這種寫法十分不方便並且容易引起bug,更加合理的作法應該是將位置信息和大小信息放在兩個不一樣的state裏面,這樣能夠避免更新某個狀態的時候要手動維護另外一個狀態:

// separate state into position and size states
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也能夠進行反作用的。那麼什麼是反作用呢?咱們能夠先來看看維基百科的定義:

In computer science, an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, that is to say has an observable effect besides returning a value (the main effect) to the invoker of the operation.

通俗來講,函數的反作用就是函數除了返回值外對外界環境形成的其它影響。舉個例子,假如咱們每次執行一個函數,該函數都會操做全局的一個變量,那麼對全局變量的操做就是這個函數的反作用。而在React的世界裏,咱們的反作用大致能夠分爲兩類,一類是調用瀏覽器的API,例如使用addEventListener來添加事件監聽函數等,另一類是發起獲取服務器數據的請求,例如當用戶卡片掛載的時候去異步獲取用戶的信息等。在Hook出來以前,若是咱們須要在組件中進行反作用的話就須要將組件寫成Class Component,而後在組件的生命週期函數裏面寫反作用,這其實會引發不少代碼設計上的問題,具體你們能夠查看個人上篇文章React爲何須要Hook。Hook出來以後,開發者就能夠在Function Component中使用useEffect來定義反作用了。雖然useEffect基本能夠覆蓋componentDidMountcomponentDidUpdatecomponentWillUnmount等生命週期函數組合起來使用的全部場景,可是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的值,從而使組件重渲染,而重渲染後useEffecteffect繼續被執行,進而組件再次重渲染。。。爲了不重複的反作用執行,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-hooksexhaustive-deps規則來進行編碼約束,在你的項目加上這個約束以後,在代碼開發階段eslint就會告訴你要將someState放到useEffectdependencies中去,這樣就能夠不使用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傳進useEffectdependencies數組的話,由於該函數頻繁被從新生成,因此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'

// this list may contain thousands of items, so each re-render is expensive
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纔是個空數組,若是你的函數有使用到外面的依賴的話,記得必定要將該依賴放進useCallbackdependencies參數中,否則會有bug發生。

注意事項

避免在函數裏面使用「舊的」變量

useEffect相似,咱們也須要將全部在useCallback的callback中使用到的外部變量寫到dependencies數組裏面,否則咱們可能會在callback調用的時候使用到「舊的」外部變量的值。

不是全部函數都要使用useCallback

Performance optimizations are not free. They ALWAYS come with a cost but do NOT always come with a benefit to offset that cost.

任何優化都會有代價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

做用

useMemouseCallback的做用十分相似,只不過它容許你記住任何類型的變量(不僅是函數)。

用法

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來記住計算的結果,當iterationsmultiplier保持不變的時候,咱們就不須要從新執行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'

// define context
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.Providervalue發生變化的時候,這個組件就會被從新渲染。這裏有一個問題就是,咱們可能會把不少不一樣的數據放在同一個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同樣經過reduceraction來管理咱們組件狀態的變換(state transition)。

用法

const [state, dispatch] = useReducer(reducer, initialArg, init?)
複製代碼

useReduceruseState相似,都是用來管理組件狀態的,只不過和useStatesetState不同的是,useReducer返回的dispatch函數是用來觸發某些改變stateaction而不是直接設置state的值,至於不一樣的action如何產生新的state的值則在reducer裏面定義。useReducer接收的三個參數分別是:

  • reducer: 這是一個函數,它的簽名是(currentState, action) => newState,從它的函數簽名能夠看出它會接收當前的state和當前dispatchaction爲參數,而後返回下一個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

useReduceruseState均可以用來管理組件的狀態,它們之間最大的區別就是,useReducer將狀態和狀態的變化統一管理在reducer函數裏面,這樣對於一些複雜的狀態管理會十分方便咱們debug,由於它對狀態的改變是封閉的。而因爲useState返回的setState能夠直接在任意地方設置咱們狀態的值,當咱們組件的狀態轉換邏輯十分複雜時,它將很難debug,由於它是開放的狀態管理。整體的來講,在useReduceruseState如何進行選擇的問題上咱們能夠參考如下這些原則:

  • 下列狀況使用useState
    • state的值是JS原始數據類型(primitives),如number, stringboolean
    • state的轉換邏輯十分簡單
    • 組件內不一樣的狀態是沒有關聯的,它們可使用多個獨立的useState來單獨管理
  • 下列狀況使用useReducer
    • state的值是object或者array
    • state的轉換邏輯十分複雜, 須要使用reducer函數來統一管理
    • 組件內多個state互相關聯,改變一個狀態時也須要改變另一個,將他們放在同一個state內使用reducer來統一管理
    • 狀態定義在父級組件,不過須要在深層次嵌套的子組件中使用和改變父組件的狀態,能夠同時使用useReduceruseContext兩個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做爲開頭,而且使用駝峯發進行命名,例如useLocationuseLocalStorageuseQueryString等等。

例子

下面舉一個最簡單的自定義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')) 複製代碼

總結

在本篇文章中我給你們介紹了React一些經常使用的內置Hook以及如何定義咱們本身的Hook。React Hook總的來講是一個十分強大的功能,合理地使用它能夠提升咱們代碼的複用率和業務代碼的開發效率,不過它也有不少隱藏的各式各樣的坑,你們在使用中必定要多加防範,個人我的建議是你們儘可能使用eslint-plugin-react-hooks插件來輔助開發,由於它真的能夠在咱們開發的過程當中就幫咱們發現代碼存在的問題,不過有時候千方百計來去掉它的警告確實是很煩人的:)。

在這個系列的下一篇文章中我將教你們如何測試咱們自定義的Hook來提升咱們的代碼質量,你們敬請期待。

參考文獻

我的技術動態

文章始發於個人我的博客

歡迎關注公衆號進擊的大蔥一塊兒學習成長

相關文章
相關標籤/搜索