React Hooks 系列之4 useReducer

本系列將講述 React Hooks 的使用方法,從 useState 開始,將包含以下內容:css

  • useState
  • useEffect
  • useContext
  • useReducer
  • useCallBack
  • useMemo
  • useRef
  • custom hooks

掌握 React Hooks api 將更好的幫助你在工做中使用,對 React 的掌握更上一層樓。本系列將使用大量實例代碼和效果展現,很是易於初學者和複習使用。react

截止目前咱們已經學習了3個hook api,useState, useEffect, useContext。接下來咱們學習下一個 hook api,useReducer。首先咱們將講講什麼是 reducer,以及爲何使用 reducer。研究一下 JavaScript 中的 reducer 是什麼,這將有助於理解 react hook 中的 useReducer。好,如今開始吧。ios

什麼是 useReducer

useReducer 是一個用於狀態管理的 hook api。是 useState 的替代方案。算法

那麼 useReduceruseState 的區別是什麼呢?答案是useState 是使用 useReducer 構建的。json

那麼何時使用 useReducer 仍是 useState 呢?咱們完成本章的學習就能找到答案。redux

reducer

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

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

在本節,咱們來看一個計數器的例子,來學習 simple state & action

  1. import useReducer api
  2. 聲明 reducer function 和 initialState
  3. 調用執行 reducer

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 的模式稍有不一樣。接下來咱們看一個稍複雜的例子。

complex state & action

下面咱們看第二個例子。將使用對象做爲 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 個計時器。

multiple useReducers

若是有多個 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 with useContext

截止目前咱們已經學習了在組件內使用 useReducer 進行狀態管理。若是在某些場景想再組件之間分享 state,進行全局的 state 管理時,咱們可使用 useReducer 加 useContext。

考慮這樣一個場景,有3個子組件A, B, C,要在子組件內控制同一個計數器,常規的寫法是將 counter 的方法寫到父組件上,而後經過 props 的方式將 counter 方法和 state 傳給子組件,子組件中調用經過 props 傳入的 counter 方法,就會改變父組件中的 state,同時也能改變做爲 props 傳遞給子組件的 app 中的 state。以下圖:

看起來沒什麼問題,可是若是組件嵌套比較深的時候,這將很是糟糕,要一層一層將 counter 方法做爲 props 傳遞給子組件。這時就要使用 useContext 加 useReducer 了。

要完成這個需求分爲 2 步

  1. 使用 useReducer 在根節點建立一個 counter 方法
  2. 經過 useContext 爲子組件提供和消費 context

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
複製代碼

頁面效果以下

咱們再一塊兒回顧一下

  1. 在 App.tsx 中,咱們使用 useReducer 建立了一個 counter,聲明瞭初始值,建立了 reducer 函數,useReducer 返回了狀態 count 和 dispatch 方法。
  2. 爲了能讓其餘組件訪問到 count 和 dispatch,咱們經過 React.createContext 建立了 CountContext,並用 <CountContext.Provider> 包裹根節點。將 count 和 dispatch 做爲 value 傳給 Provider。
  3. 在子節點中,咱們使用 useContext 獲取到 count 和 dispatch 方法,經過調用 dispatch 實現對 count 的改變。

Fetching Data with useReducer

以前咱們已經在 useEffect 的章節中學習掌握瞭如何請求數據,當時是使用了 useEffect 和 useState,如今咱們再來看看如何使用 useReducer 去請求遠程數據。

接下來咱們作這樣一個小需求:

  1. 頁面載入時請求數據
  2. 請求數據中展現 loading 狀態
  3. 請求返回後移除 loading 樣式,展現請求的數據;若請求失敗,也移除 loading 展現錯誤提示

咱們將分別使用 useState 和 useReducer 來實現,並對比其中的區別。

useState 實現請求

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 實現。

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,咱們繼續往下看。

useState vs useReducer

  • 若是 state 的類型爲 Number, String, Boolean 建議使用 useState,若是 state 的類型 爲 Object 或 Array,建議使用 useReducer
  • 若是 state 變化很是多,也是建議使用 useReducer,集中管理 state 變化,便於維護
  • 若是 state 關聯變化,建議使用 useReducer
  • 業務邏輯若是很複雜,也建議使用 useReducer
  • 若是 state 只想用在 組件內部,建議使用 useState,若是想維護全局 state 建議使用 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,並給出了使用建議。

相關文章
相關標籤/搜索