本系列將講述 React Hooks 的使用方法,從 useState 開始,將包含以下內容:css
掌握 React Hooks api 將更好的幫助你在工做中使用,對 React 的掌握更上一層樓。本系列將使用大量實例代碼和效果展現,很是易於初學者和複習使用。react
截止目前咱們已經學習了3個hook api,useState
, useEffect
, useContext
。接下來咱們學習下一個 hook api,useReducer
。首先咱們將講講什麼是 reducer,以及爲何使用 reducer。研究一下 JavaScript 中的 reducer 是什麼,這將有助於理解 react hook 中的 useReducer
。好,如今開始吧。ios
useReducer
是一個用於狀態管理的 hook api。是 useState
的替代方案。算法
那麼 useReducer
和 useState
的區別是什麼呢?答案是useState
是使用 useReducer
構建的。json
那麼何時使用 useReducer
仍是 useState
呢?咱們完成本章的學習就能找到答案。redux
useState - state
useEffect - side effects
useContext - context API
useReducer - reducers
複製代碼
能夠看到 useReducer 必定也與 reducer 有關,接下來看看什麼是 reducer。之因此要了解什麼是 reducer 是爲了你不須要掌握 redux 而學會 useReducer,固然,若是你瞭解Redux,對本章理解會更容易。下面開始axios
若是你研究過原生 JavaScript,你會發現有一些內置方法,如 foreach
, map
, reduce
。咱們來深刻看一下 reduce
方法。在 MDN 上能夠看到 Array.prototype.reduce()
的文檔,文檔中說api
reduce() 方法對數組中的每一個元素執行一個由您提供的 reducer 函數(升序執行),將其結果彙總爲單個返回值。數組
const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;
// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10
// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5));
// expected output: 15
複製代碼
須要注意的是:reduce 方法接受2個參數,第一個爲 reducer 函數,第二個爲初始值(給 reducer 函數使用)。reduce 方法返回函數累計處理的結果。bash
而 reducer 函數有2個必填入參:
accumulator
累計器累計回調的返回值; 它是上一次調用回調時返回的累積值,或 initialValue。currentValue
數組中正在處理的元素。reducer 與 useReducer 這二者之間有巨大的類似之處。
reduce in JavaScript | useReducer in React |
---|---|
array.reduce(reducer, initialValue) |
useReducer(reducer, initialState) |
singleValue = reducer(accumulator, itemValue) |
newState = reducer(currentState, action) |
reduce method returns a single value | useReducer returns a pair of values. [newState, dispatch] |
上述表格目前看不懂也沒有關係,後續經過例子會詳細說明。
在這一小節咱們學到了:
useReducer
是一個用於狀態管理的 hook api。useReducer
與 reducer 函數有關useReducer(reducer, initialState)
接受2個參數,分別爲 reducer 函數 和 初始狀態reducer(currentState, action)
也是接受2個參數,分別爲當前狀態和 action,返回一個 new state在本節,咱們來看一個計數器的例子,來學習 simple state & action
CounterOne.tsx
import React, { useReducer } from 'react'
const initialState = 0
const reducer = (state: number, action: string) => {
switch (action) {
case 'increment':
return state + 1
case 'decrement':
return state - 1
case 'reset':
return initialState
default:
return state
}
}
function CounterOne() {
const [count, dispatch] = useReducer(reducer, initialState)
return (
<div> <div>Count - {count}</div> <button onClick={() => dispatch('increment')} >Increment</button> <button onClick={() => dispatch('decrement')} >Decrement</button> <button onClick={() => dispatch('reset')} >Reset</button> </div>
)
}
export default CounterOne
複製代碼
App.tsx
import React from 'react'
import './App.css'
import CounterOne from './components/19CounterOne'
const App = () => {
return (
<div className="App"> <CounterOne /> </div>
)
}
export default App
複製代碼
頁面展現以下:
在回顧一下代碼,首先 import useReducer
import React, { useReducer } from 'react'
複製代碼
而後調用 useReducer
const [count, dispatch] = useReducer(reducer, initialState)
複製代碼
聲明 reducer, initialState
const initialState = 0
const reducer = (state: number, action: string) => {
switch (action) {
case 'increment':
return state + 1
case 'decrement':
return state - 1
case 'reset':
return initialState
default:
return state
}
}
複製代碼
reducer function 的2個參數,分別爲當前 state 和 action, 並根據不一樣的 action 返回不一樣的新的 state。
useReducer 返回了一個數組,2個元素分別爲 state 和 dispatch 方法。其中 state 在咱們的例子中就是當前的 count 值,dispatch 方法接受一個參數,執行對應的 action。dispatch 執行後,對應的 state 會改變,組件會 rerender,來展現最新的狀態。
這就是使用 simple state 和 simple action 的例子,本例中 state 是一個 number 類型,action 也是簡單的 string 類型,這和 Redux 的模式稍有不一樣。接下來咱們看一個稍複雜的例子。
下面咱們看第二個例子。將使用對象做爲 state 和 action 的值,這就比較相似 Redux 的模式了。
CounterTwo.tsx
import React, { useReducer } from 'react'
const initialState = {
firstCounter: 0
}
const reducer = (
state: {
firstCounter: number
},
action: {
type: string
}
) => {
switch (action.type) {
case 'increment':
return {
firstCounter: state.firstCounter + 1
}
case 'decrement':
return {
firstCounter: state.firstCounter - 1
}
case 'reset':
return initialState
default:
return state
}
}
function CounterTwo() {
const [count, dispatch] = useReducer(reducer, initialState)
return (
<div> <div>Count - {count.firstCounter}</div> <button onClick={() => dispatch({ type: 'increment' })} >Increment</button> <button onClick={() => dispatch({ type: 'decrement' })} >Decrement</button> <button onClick={() => dispatch({ type: 'reset' })} >Reset</button> </div>
)
}
export default CounterTwo
複製代碼
App.tsx
import React from 'react'
import './App.css'
import CounterTwo from './components/20CountTwo'
const App = () => {
return (
<div className="App"> <CounterTwo /> </div>
)
}
export default App
複製代碼
頁面展現以下:
與上一節的示例效果相同。如今,咱們已經將 state 和 action 都改寫爲對象了,那麼這樣寫有什麼好處呢?
其一的好處是 action 如今是一個對象了,能夠有多個屬性決定 action 的效果。例如咱們再添加一個 +5 的邏輯。
CounterTwo.tsx
import React, { useReducer } from 'react'
const initialState = {
firstCounter: 0
}
const reducer = (
state: {
firstCounter: number
},
action: {
type: string
value: number
}
) => {
switch (action.type) {
case 'increment':
return {
firstCounter: state.firstCounter + action.value
}
case 'decrement':
return {
firstCounter: state.firstCounter - action.value
}
case 'reset':
return initialState
default:
return state
}
}
function CounterTwo() {
const [count, dispatch] = useReducer(reducer, initialState)
return (
<div> <div>Count - {count.firstCounter}</div> <button onClick={() => dispatch({ type: 'increment', value: 1 })} >Increment</button> <button onClick={() => dispatch({ type: 'decrement', value: 1 })} >Decrement</button> <button onClick={() => dispatch({ type: 'increment', value: 5 })} >Increment 5</button> <button onClick={() => dispatch({ type: 'decrement', value: 5 })} >Decrement 5</button> <button onClick={() => dispatch({ type: 'reset', value: 0})} >Reset</button> </div>
)
}
export default CounterTwo
複製代碼
頁面展現以下:
能夠注意到給 action 增長了一個 value 屬性,實現了加減 5 的邏輯。
第二個好處是 state 做爲一個對象,就能夠添加更多的 state 屬性,例如咱們在增長一個計數器2,代碼以下:
import React, { useReducer } from 'react'
const initialState = {
firstCounter: 0,
secondCounter: 10,
}
const reducer = (
state: {
firstCounter: number
secondCounter: number
},
action: {
type: string
value: number
}
) => {
switch (action.type) {
case 'increment':
return {
...state,
firstCounter: state.firstCounter + action.value
}
case 'decrement':
return {
...state,
firstCounter: state.firstCounter - action.value
}
case 'increment2':
return {
...state,
secondCounter: state.secondCounter + action.value
}
case 'decrement2':
return {
...state,
secondCounter: state.secondCounter - action.value
}
case 'reset':
return initialState
default:
return state
}
}
function CounterTwo() {
const [count, dispatch] = useReducer(reducer, initialState)
return (
<div> <div>First Count - {count.firstCounter}</div> <div>Second Count - {count.secondCounter}</div> <button onClick={() => dispatch({ type: 'increment', value: 1 })} >Increment</button> <button onClick={() => dispatch({ type: 'decrement', value: 1 })} >Decrement</button> <button onClick={() => dispatch({ type: 'increment', value: 5 })} >Increment 5</button> <button onClick={() => dispatch({ type: 'decrement', value: 5 })} >Decrement 5</button> <div> <button onClick={() => dispatch({ type: 'increment2', value: 1 })} >Increment second</button> <button onClick={() => dispatch({ type: 'decrement2', value: 1 })} >Decrement second</button> </div> <button onClick={() => dispatch({ type: 'reset', value: 0 })} >Reset</button> </div>
)
}
export default CounterTwo
複製代碼
頁面展現效果以下
這樣咱們就能同時維護 2 個計時器。
若是有多個 state,但 state 變化的方式又是相同的時候,能夠屢次使用 useReducer。
CounterThree.tsx
import React, { useReducer } from 'react'
const initialState = 0
const reducer = (state: number, action: string) => {
switch (action) {
case 'increment':
return state + 1
case 'decrement':
return state - 1
case 'reset':
return initialState
default:
return state
}
}
function CounterThree() {
const [count, dispatch] = useReducer(reducer, initialState)
const [countTwo, dispatchTwo] = useReducer(reducer, initialState)
return (
<div> <div>Count - {count}</div> <button onClick={() => dispatch('increment')} >Increment</button> <button onClick={() => dispatch('decrement')} >Decrement</button> <button onClick={() => dispatch('reset')} >Reset</button> <br/> <div>CountTwo - {countTwo}</div> <button onClick={() => dispatchTwo('increment')} >Increment</button> <button onClick={() => dispatchTwo('decrement')} >Decrement</button> <button onClick={() => dispatchTwo('reset')} >Reset</button> </div>
)
}
export default CounterThree
複製代碼
頁面展現以下
這個例子中使用了多個 useReducer,但共用了一個 reducer function。這有效的避免了合併對象的麻煩(能夠對比上一節使用展開運算法合併 state)。也提升了代碼的複用性。
截止目前咱們已經學習了在組件內使用 useReducer 進行狀態管理。若是在某些場景想再組件之間分享 state,進行全局的 state 管理時,咱們可使用 useReducer 加 useContext。
考慮這樣一個場景,有3個子組件A, B, C,要在子組件內控制同一個計數器,常規的寫法是將 counter 的方法寫到父組件上,而後經過 props 的方式將 counter 方法和 state 傳給子組件,子組件中調用經過 props 傳入的 counter 方法,就會改變父組件中的 state,同時也能改變做爲 props 傳遞給子組件的 app 中的 state。以下圖:
看起來沒什麼問題,可是若是組件嵌套比較深的時候,這將很是糟糕,要一層一層將 counter 方法做爲 props 傳遞給子組件。這時就要使用 useContext 加 useReducer 了。
要完成這個需求分爲 2 步
App.tsx
import React, { useReducer } from 'react'
import './App.css'
import A from './components/22A'
import B from './components/22B'
import C from './components/22C'
interface CountContextType {
countState: number
countDispatch: (action: string) => void
}
export const CountContext = React.createContext({} as CountContextType)
const initialState = 0
const reducer = (state: number, action: string) => {
switch (action) {
case 'increment':
return state + 1
case 'decrement':
return state - 1
case 'reset':
return initialState
default:
return state
}
}
const App = () => {
const [count, dispatch] = useReducer(reducer, initialState)
return (
<CountContext.Provider value={{ countState: count, countDispatch: dispatch, }} > <div className="App"> Count - {count} <A /> <B /> <C /> </div> </CountContext.Provider> ) } export default App 複製代碼
A.tsx
import React, { useContext } from 'react'
import { CountContext } from '../App'
function A() {
const countContext = useContext(CountContext)
return (
<div> A - {countContext.countState} <button onClick={() => countContext.countDispatch('increment')} >Increment</button> <button onClick={() => countContext.countDispatch('decrement')} >Decrement</button> <button onClick={() => countContext.countDispatch('reset')} >Reset</button> </div>
)
}
export default A
複製代碼
B.tsx
import React from 'react'
import D from './22D'
function B() {
return (
<div> <D /> </div>
)
}
export default B
複製代碼
C.tsx
import React from 'react'
import E from './22E'
function C() {
return (
<div> <E /> </div>
)
}
export default C
複製代碼
D.tsx
import React, { useContext } from 'react'
import { CountContext } from '../App'
function D() {
const countContext = useContext(CountContext)
return (
<div> D - {countContext.countState} <button onClick={() => countContext.countDispatch('increment')} >Increment</button> <button onClick={() => countContext.countDispatch('decrement')} >Decrement</button> <button onClick={() => countContext.countDispatch('reset')} >Reset</button> </div>
)
}
export default D
複製代碼
E.tsx
import React from 'react'
import F from './22F'
function E() {
return (
<div> <F /> </div>
)
}
export default E
複製代碼
F.tsx
import React, { useContext } from 'react'
import { CountContext } from '../App'
function F() {
const countContext = useContext(CountContext)
return (
<div> F - {countContext.countState} <button onClick={() => countContext.countDispatch('increment')} >Increment</button> <button onClick={() => countContext.countDispatch('decrement')} >Decrement</button> <button onClick={() => countContext.countDispatch('reset')} >Reset</button> </div>
)
}
export default F
複製代碼
頁面效果以下
咱們再一塊兒回顧一下
<CountContext.Provider>
包裹根節點。將 count 和 dispatch 做爲 value 傳給 Provider。以前咱們已經在 useEffect 的章節中學習掌握瞭如何請求數據,當時是使用了 useEffect 和 useState,如今咱們再來看看如何使用 useReducer 去請求遠程數據。
接下來咱們作這樣一個小需求:
咱們將分別使用 useState 和 useReducer 來實現,並對比其中的區別。
App.tsx
import React from 'react'
import './App.css'
import DataFetchingOne from './components/23DataFetchingOne'
const App = () => {
return (
<div className="App"> <DataFetchingOne /> </div>
)
}
export default App
複製代碼
DataFetchingOne.tsx
import React, { useState, useEffect } from 'react'
import axios from 'axios'
interface postType {
userId: number
id: number
title: string
body: string
}
function DataFetchingOne() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [post, setPost] = useState({} as postType)
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/posts/1').then((res) => {
setLoading(false)
setPost(res.data)
setError('')
}).catch(() => {
setLoading(false)
setPost({} as postType)
setError('something went wrong')
})
}, [])
return (
<div> { loading ? 'Loading...' : post.title } { error ? error : null } </div>
)
}
export default DataFetchingOne
複製代碼
頁面效果以下
咱們故意改錯一個 axios 請求的連接,能夠看到以下進入錯誤的邏輯。
注意到在這個實現中,咱們使用了3個useState去控制 loading, post 和 error,接下來看看如何使用 useReducer 實現。
App.tsx
import React from 'react'
import './App.css'
import DataFetchingOne from './components/23DataFetchingOne'
const App = () => {
return (
<div className="App"> <DataFetchingOne /> </div>
)
}
export default App
複製代碼
import React, { useEffect, useReducer } from 'react'
import axios from 'axios'
interface postType {
userId: number
id: number
title: string
body: string
}
type stateType = {
loading: boolean
error: string
post?: postType | {}
}
type actionType = {
type: 'FETCH_SUCCESS' | 'FETCH_ERROR'
payload?: postType | {}
}
const initialState = {
loading: true,
error: '',
post: {},
}
const reducer = (state: stateType, action: actionType) => {
switch (action.type) {
case 'FETCH_SUCCESS':
return {
loading: false,
error: '',
post: action.payload,
}
case 'FETCH_ERROR':
return {
loading: false,
error: 'something went wrong',
post: {},
}
default:
return state
}
}
function DataFetchingTwo() {
const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/posts/1').then((res) => {
dispatch({
type: 'FETCH_SUCCESS',
payload: res.data,
})
}).catch(() => {
dispatch({
type: 'FETCH_ERROR'
})
})
}, [])
return (
<div> { state.loading ? 'Loading...' // @ts-ignore : state.post.title } { state.error ? state.error : null } </div>
)
}
export default DataFetchingTwo
複製代碼
頁面展現效果與上一個例子相同
能夠看到,咱們將 state 集合在了一塊兒,在同一個對象,修改 state 的邏輯也聚合在了一塊兒,即 reducer 函數中的 switch 部分。
至此你可能會好奇,何時該用 useState 何時該用 useReducer,咱們繼續往下看。
Scenario | useState | useReducer |
---|---|---|
Type of state | Number, String, Boolean | Object or Array |
Number of state transitions | 1 or 2 | Too many |
Related state transitions | No | Yes |
Business logic | No business logic | Complex business logic |
local vs global | local | global |
本章主要講述了 useReducer 的使用方法。從 JavaScript 中的 reduce api 開始,對比說明了什麼是 useReducer。
學習了 useReducer 的簡單狀態的使用,複雜狀態的使用,以及多個 useReducer 的使用。
掌握了組件嵌套多層時使用 useContext 加 useReducer 完成子組件修改全局state的方法,代碼更優雅,可維護性更高。
經過對比 useState,學習瞭如何使用 useEffect 加 useReducer 請求數據,並控制 loading 狀態的顯示隱藏。
最後對比了 useState 和 useReducer,並給出了使用建議。