React Hooks серии 4 useReducer

React.js
React Hooks серии 4 useReducer

В этой серии будет описано, как использовать React Hooks, начиная с useState, и будет рассмотрено следующее:

Освоение API React Hooks поможет вам лучше использовать его в своей работе, а ваше владение React перейдет на новый уровень. В этой серии будет использоваться много примеров кода и демонстраций эффектов, которые очень просты в использовании для новичков и рецензентов.

На данный момент мы изучили 3 хука api,useState, useEffect, useContext. Далее изучаем следующий хук api,useReducer. Сначала поговорим о том, что такое редуктор и для чего он используется. Взгляните, что такое редюсер в JavaScript, это поможет понять, что такое хуки реакции.useReducer. Хорошо, давайте начнем сейчас.

что такое useReducer

useReducerэто хук API для управления состоянием. даuseStateальтернатива.

ТакuseReducerа такжеuseStateВ чем разница? ответuseStateэто использоватьuseReducerпостроен.

Итак, когда использоватьuseReducerещеuseStateШерстяная ткань? Мы можем найти ответ, завершив изучение этой главы.

reducer

useState - state
useEffect - side effects
useContext - context API
useReducer - reducers

Вы можете видеть, что useReducer также должен быть связан с редуктором, давайте посмотрим, что такое редюсер. Причина, по которой вам нужно знать, что такое редьюсер, заключается в том, что вам не нужно знать редукс, чтобы научиться использовать редьюсер.Конечно, если вы знаете редукс, вам будет легче понять эту главу. начать ниже

Если вы изучали родной JavaScript, вы обнаружите, что есть встроенные методы, такие какforeach, map, reduce. Давайте посмотрим поближеreduceметод. Доступно на MDNArray.prototype.reduce()документация, в документации сказано

Метод reduce() выполняет функцию редуктора (выполняется в порядке возрастания), предоставленную вами для каждого элемента в массиве, суммируя ее результаты в одно возвращаемое значение.

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

Следует отметить, что метод сокращения принимает 2 параметра, первый — это функция редуктора, а второй — начальное значение (используемое функцией редуктора). Метод сокращения возвращает результат кумулятивной обработки функции.

Функция редуктора имеет 2 обязательных параметра:

  • accumulatorАккумулятор накапливает возвращаемое значение обратного вызова; это накопленное значение, возвращенное при последнем вызове обратного вызова, или initialValue.
  • currentValueЭлемент массива, который обрабатывается.

Редуктор и useReducer

Между редьюсером и 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это хук API для управления состоянием.
  • useReducerсвязанные с редукторными функциями
  • useReducer(reducer, initialState)Принимает 2 параметра: функцию редуктора и начальное состояние.
  • reducer(currentState, action)Он также принимает 2 параметра, а именно текущее состояние и действие, и возвращает новое состояние.

simple state & action

В этом разделе мы рассмотрим встречный пример, чтобы изучить простое состояние и действие.

  1. import useReducer api
  2. объявить функцию редуктора и InitialState
  3. вызов для выполнения редуктора

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

Страница выглядит следующим образом:

После просмотра кода сначала импортируйте useReducer

import React, { useReducer } from 'react'

Затем вызовите useReducer

const [count, dispatch] = useReducer(reducer, initialState)

объявить редуктор, 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
  }
}

Двумя параметрами функции редуктора являются текущее состояние и действие, и они возвращают разные новые состояния в соответствии с разными действиями.

useReducer возвращает массив с двумя элементами для методов состояния и отправки. Состояние в нашем случае — это текущее значение счетчика, а метод диспетчеризации принимает параметр и выполняет соответствующее действие. После выполнения отправки соответствующее состояние изменится, и компонент будет повторно отображаться для отображения последнего состояния.

Это пример использования простого состояния и простого действия, в данном случае состояние — это числовой тип, а действие — простой строковый тип, который немного отличается от шаблона Redux. Далее мы рассмотрим немного более сложный пример.

complex 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

Страница выглядит следующим образом:

Тот же эффект, что и в примере из предыдущего раздела. Теперь, когда мы переписали и состояние, и действие как объекты, какая от этого польза?

Одним из преимуществ является то, что действие теперь является объектом и может иметь несколько свойств, определяющих эффект действия. Например, давайте добавим еще одну логику +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

Страница выглядит следующим образом:

Можно заметить, что к действию добавлен атрибут value для реализации логики прибавления или вычитания 5.

Второе преимущество заключается в том, что в качестве объекта можно добавить больше свойств состояния, например, мы добавляем счетчик 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

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

Страница отображается следующим образом

В этом примере используется несколько useReducers, но используется общая функция редьюсера. Это позволяет избежать проблем с объединением объектов (сравните использование алгоритма раскрутки для состояния слияния в предыдущем разделе). Это также улучшает возможность повторного использования кода.

useReducer with useContext

До сих пор мы научились использовать useReducer для управления состоянием внутри компонента. Если мы хотим разделить состояние между компонентами в некоторых сценариях и выполнить глобальное управление состоянием, мы можем использовать useReducer и useContext.

Рассмотрим такой сценарий, есть 3 подкомпонента A, B, C, для управления одним и тем же счетчиком в подкомпоненте, обычный способ записи - записать метод счетчика в родительский компонент, а затем передать метод счетчика и состояние через реквизиты.Для дочернего компонента вызов метода счетчика, переданного через реквизиты в дочернем компоненте, изменит состояние в родительском компоненте, а также состояние в приложении, переданном дочернему компоненту в качестве реквизита. Как показано ниже:

Вроде бы все хорошо, но если компоненты глубоко вложены, это будет очень плохо, и вам придется передавать метод счетчика в качестве реквизита дочерним компонентам слой за слоем. На данный момент нам нужно использовать useContext и useReducer.

Выполнение этого требования разделено на 2 этапа.

  1. Создайте метод счетчика на корневом узле, используя useReducer
  2. Предоставление и использование контекста дочерним компонентам через useContext

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 для создания счетчика, объявления начального значения, создания функции редуктора, useReducer возвращает количество состояний и метод отправки.
  2. Чтобы разрешить другим компонентам доступ к подсчету и отправке, мы создаем CountContext через React.createContext и используем<CountContext.Provider>Оберните корневой узел. Передайте количество и отправьте как значение провайдеру.
  3. В дочернем узле мы используем useContext для получения методов подсчета и отправки, а также меняем подсчет, вызывая отправку.

Fetching Data with useReducer

Мы узнали, как запрашивать данные в главе useEffect раньше, используя useEffect и useState в то время, теперь давайте посмотрим, как использовать useReducer для запроса удаленных данных.

Далее делаем такое небольшое требование:

  1. Запросить данные при загрузке страницы
  2. Показать состояние загрузки в данных запроса
  3. После возврата запроса удалите стиль загрузки и отобразите запрошенные данные; если запрос не выполнен, удалите загрузку и отобразите сообщение об ошибке

Мы будем использовать 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 useStates для управления загрузкой, публикацией и ошибкой, давайте посмотрим, как использовать реализацию 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

Эффект отображения страницы такой же, как и в предыдущем примере.

Как видите, мы агрегируем состояние вместе, и в этом же объекте вместе агрегируется и логика модификации состояния, то есть переключающая часть функции редуктора.

На данный момент вам может быть интересно, когда использовать useState и когда использовать useReducer, давайте продолжим смотреть.

useState vs useReducer

  • Если тип состояния Number, String, Boolean, рекомендуется использовать useState, если тип состояния Object или Array, рекомендуется использовать useReducer
  • Если состояние сильно меняется, также рекомендуется использовать useReducer для централизованного управления изменениями состояния и облегчения обслуживания.
  • Если ассоциация состояния изменяется, рекомендуется использовать useReducer
  • Если бизнес-логика очень сложная, также рекомендуется использовать useReducer.
  • Если состояние используется только внутри компонента, рекомендуется использовать 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. Начиная с API сокращения в JavaScript, сравнение показывает, что такое useReducer.

Изучил использование простых состояний useReducer, использование сложных состояний и использование нескольких useReducer.

Я освоил метод использования useContext и useReducer для изменения глобального состояния подкомпонентов, когда компоненты вложены в несколько слоев, код стал более элегантным и удобным в сопровождении.

Сравнив useState, я узнал, как использовать useEffect и useReducer для запроса данных и управления отображением и скрытием состояния загрузки.

Наконец, сравниваются useState и useReducer и даются предложения по использованию.