在React爲何須要Hook這篇文章中咱們探討了React開發團隊爲何要爲Function Component添加Hook的緣由,在本篇文章中我將會爲你們提供一份較爲全面的React Hook實踐指南,其中包括如下方面的內容:javascript
React Hook是React 16.8版本以後添加的新屬性,用最簡單的話來講,React Hook就是一些React提供的內置函數,這些函數可讓Function Component和Class Component同樣可以擁有組件狀態(state)以及進行反作用(side effect)。html
接下來我將會爲你們介紹一些經常使用的Hook,對於每個Hook,我都會覆蓋如下方面的內容:java
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
的函數。這裏要注意的是state
和setState
這兩個變量的命名不是固定的,應該根據你業務的實際狀況選擇不一樣的名字,能夠是text
和setText
,也能夠是width
和setWidth
這類的命名。(對上面數組解構賦值不熟悉的同窗能夠看下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相似,setCounter
和setText
均可以接收一個函數爲參數,這個函數叫作updater
,updater
接收的參數是當前狀態的最新值,返回值是下一個狀態。例如setCounter的參數能夠改爲一個函數:github
<button
onClick={() => {
setCounter(counter => counter + 1)
}}
>
Increase counter
</button>
複製代碼
useState
的initialState
也能夠是一個用來生成狀態初始值的函數,這種作法主要是避免組件每次渲染的時候initialState
須要被重複計算。下面是個簡單的例子:express
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props)
return initialState
})
複製代碼
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來管理你的狀態,這樣你的代碼會更好維護。
若是setState接收到的新的state
和當前的state
是同樣的(判斷方法是Object.is),React將不會從新渲染子組件或者觸發side effect
。這裏要注意的是雖然React不會渲染子組件,不過它仍是會從新渲染當前的組件的,若是你的組件渲染有些很耗性能的計算的話,能夠考慮使用useMemo來優化性能。
不管是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
是用來使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
基本能夠覆蓋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
是用來在組件不一樣渲染之間共用一些數據的,它的做用和咱們在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}
對象,只不過它能夠保證在組件每次渲染的時候拿到的都是同一個對象。
useRef
返回的ref object被從新賦值的時候不會引發組件的重渲染,若是你有這個需求的話請使用useState
來存儲數據。
隨着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'
// 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
纔是個空數組,若是你的函數有使用到外面的依賴的話,記得必定要將該依賴放進useCallback
的dependencies
參數中,否則會有bug發生。
和useEffect
相似,咱們也須要將全部在useCallback
的callback中使用到的外部變量寫到dependencies
數組裏面,否則咱們可能會在callback
調用的時候使用到「舊的」外部變量的值。
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
和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函數來從新計算了,直接使用上一次的結果便可。
和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')) 複製代碼
咱們知道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.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組件,因此這些無用的渲染會影響到咱們頁面的性能,解決上面這個問題的方法有下面三種:
這個方法是最被推薦的作法,和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'))
複製代碼
若是出於某些緣由你不能拆分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
來將上面的代碼進行優化,代碼以下:
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
用最簡單的話來講就是容許咱們在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
接收的三個參數分別是:
(currentState, action) => newState
,從它的函數簽名能夠看出它會接收當前的state和當前dispatch
的action
爲參數,而後返回下一個state,也就是說它負責狀態轉換(state transition)的工做。init
參數,這個參數表明的是這個reducer
的初始狀態,若是init
參數有被指定的話,initialArg
會被做爲參數傳進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
和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
useReducer
useReducer
上面介紹了React內置的經常使用Hook的用法,接着咱們看一下如何編寫咱們本身的Hook。
自定義Hook的目的是讓咱們封裝一些能夠在不一樣組件之間共用的非UI邏輯來提升咱們開發業務代碼的效率。
以前咱們說過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')) 複製代碼
在本篇文章中我給你們介紹了React一些經常使用的內置Hook以及如何定義咱們本身的Hook。React Hook總的來講是一個十分強大的功能,合理地使用它能夠提升咱們代碼的複用率和業務代碼的開發效率,不過它也有不少隱藏的各式各樣的坑,你們在使用中必定要多加防範,個人我的建議是你們儘可能使用eslint-plugin-react-hooks
插件來輔助開發,由於它真的能夠在咱們開發的過程當中就幫咱們發現代碼存在的問題,不過有時候千方百計來去掉它的警告確實是很煩人的:)。
在這個系列的下一篇文章中我將教你們如何測試咱們自定義的Hook來提升咱們的代碼質量,你們敬請期待。
文章始發於個人我的博客
歡迎關注公衆號進擊的大蔥一塊兒學習成長