This series will cover the use of React Hooks, starting with useState, and will include the following:

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

Mastering the React Hooks API will help you better use it in your work and get to the next level of mastering React. This series uses a lot of sample code and effects that are easy to use for beginners and review.

So far we have studied three hook apis, useState, useEffect and useContext. Let’s look at the next hook API, useReducer. First we’ll talk about what reducers are and why they should be used. Look at what reducer is in JavaScript, which will help you understand the useReducer in React Hooks. Okay, let’s get started.

What is a useReducer

UseReducer is a Hook API for state management. Is an alternative to useState.

So what’s the difference between useReducer and useState? The answer is that useState is built using useReducer.

So when to use useReducer or useState? We’ll find out by the end of this chapter.

reducer

useState - state
useEffect - side effects
useContext - context API
useReducer - reducers
Copy the code

You can see that useReducer must also be related to reducer, so let’s see what reducer is. The reason why we need to know what Reducer is is so that you can learn useReducer without redux. Of course, if you know redux, it will be easier to understand this chapter. The following start

If you’ve studied native JavaScript, you’ll find that there are built-in methods such as Foreach, Map, and Reduce. Let’s take a closer look at the Reduce method. You can see the documentation for array.prototype.reduce () on MDN, which says

The reduce() method executes a Reducer function (ascending) that you provide for each element in the array, summarizing its results into a single return value.

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
Copy the code

Note that the reduce method takes two parameters: the first is the reducer function, and the second is the initial value (used by the reducer function). The reduce method returns the result of cumulative processing by the function.

The reducer function has two required parameters:

  • accumulatorThe accumulator accumulates the return value of the callback; It is the cumulative value, or initialValue, that was returned when the callback was last called.
  • currentValueThe element in the array being processed.

Reducer and useReducer

There are great similarities between reducer and 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]

It doesn’t matter if you can’t understand the above table at present. I will explain it in detail through examples later.

In this section we learned:

  • useReducerIs a hook API for state management.
  • useReducerRelated to the Reducer function
  • useReducer(reducer, initialState)Accept two parameters, namely the reducer function and the initial state
  • reducer(currentState, action)It also takes two arguments, the current state and the action, and returns a new state

simple state & action

In this section, we’ll look at an example counter to learn about Simple State & Action

  1. import useReducer api
  2. Declare the Reducer function and initialState
  3. Call executor

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

Copy the code

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
Copy the code

The page is displayed as follows:

To review the code, first import useReducer

import React, { useReducer } from 'react'
Copy the code

And then we call useReducer

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

Statement 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
  }
}
Copy the code

The two parameters of the reducer function are the current state and action respectively, and different new states are returned according to different actions.

UseReducer returns an array with two elements for the state and Dispatch methods. Where state in our example is the current count value, the dispatch method takes an argument and executes the corresponding action. After dispatch executes, the corresponding state will change, and the component will rerender to show the latest state.

This is an example of using simple State and Simple Action. In this case, state is a number and action is a simple string, which is slightly different from the Redux pattern. Now let’s look at a slightly more complicated example.

complex state & action

Let’s look at the second example. Objects will be used as the values for state and action, which is more like the pattern in 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
Copy the code

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
Copy the code

The page is displayed as follows:

This is the same as the example in the previous section. Now that we’ve rewritten both state and action as objects, what’s the benefit?

The first advantage is that the action is now an object and can have multiple properties that determine the effect of the action. For example, let’s add another +5 logic.

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
Copy the code

The page is displayed as follows:

Notice that we added a value attribute to the action, implementing the logic of adding or subtracting 5.

The second advantage is that as state is an object, we can add more state properties. For example, we are adding a counter 2, as follows:

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
Copy the code

The following is the display effect

This allows us to maintain both timers simultaneously.

multiple useReducers

If there are multiple states, but the state changes in the same way, you can use useReducer multiple times.

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
Copy the code

The page is shown as follows

This example uses multiple Usereducers but shares a Reducer function. This effectively eliminates the hassle of merging objects (as opposed to using the expansion method to merge states in the previous section). It also improves code reusability.

useReducer with useContext

So far we’ve learned how to use useReducer for state management within a component. If you want to share state between components in some scenarios and manage state globally, we can use useReducer plus useContext.

Consider A scenario where you have three child components A, B, and C, and you want to control the same counter within the child component. The normal way to write this is to write the method counter to the parent component, and then pass the counter method and state to the child component by means of props. Calling the counter method passed in as props from a child component changes the state in the parent component. It also changes the state in the app passed as props to the child component. The diagram below:

This may seem fine, but it can be very bad if the components are deeply nested, passing counter methods as props to child components one level at a time. This is where you use useContext plus useReducer.

There are two steps to completing this requirement

  1. Create a counter method on the root node using useReducer
  2. Context is provided and consumed for child components through 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
Copy the code

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
Copy the code

B.tsx

import React from 'react'
import D from './22D'

function B() {
  return (
    <div>
      <D />
    </div>)}export default B
Copy the code

C.tsx

import React from 'react'
import E from './22E'

function C() {
  return (
    <div>
      <E />
    </div>)}export default C
Copy the code

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
Copy the code

E.tsx

import React from 'react'

import F from './22F'

function E() {
  return (
    <div>
      <F />
    </div>)}export default E
Copy the code

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
Copy the code

The page looks like this

Let’s review it again

  1. In app. TSX, we use useReducer to create a counter, declare the initial value, and create the reducer function. UseReducer returns the status count and dispatch methods.
  2. To enable other components to access the count and Dispatch, we created a CountContext with react.createcontext and used<CountContext.Provider>Wrap the root node. Pass count and Dispatch as values to the Provider.
  3. In the child node, we use useContext to get the count and dispatch methods and change the count by calling Dispatch.

Fetching Data with useReducer

We’ve already learned how to request data in the useEffect chapter, using useEffect and useState. Now let’s look at how to request remote data using useReducer.

Next we do this little requirement:

  1. The data is requested when the page loads
  2. The loading state is displayed in the request data
  3. After the request returns, remove the loading style and display the requested data. If the request fails, remove Loading and display an error message

We’ll use useState and useReducer, respectively, and compare the differences.

UseState implements the request

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
Copy the code

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
Copy the code

The page looks like this

We intentionally fixed the link to an AXIos request and can see the following logic entering the error.

Note that in this implementation we use three usestates to control loading, POST, and error. Let’s see how to implement this using useReducer.

UseReducer implements the request

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
Copy the code
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
Copy the code

The page presentation is the same as in the previous example

As you can see, we have gathered the state together. In the same object, the logic to modify the state is also aggregated, that is, the switch part in the Reducer function.

So now you might be wondering when to use useState and when to use useReducer, so let’s move on.

useState vs useReducer

  • If the state type is Number, String, or Boolean, useState is recommended. If the state type is Object or Array, useReducer is recommended
  • If state changes very much, it is also recommended to use useReducer to centrally manage state changes and facilitate maintenance
  • If the state association changes, useReducer is recommended
  • UseReducer is also recommended if the business logic is complex
  • UseState is recommended if state is only intended for internal components, and useReducer is recommended if you want to maintain global state
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

summary

This chapter mainly describes how to use useReducer. Starting with the Reduce API in JavaScript, the comparison illustrates what a useReducer is.

I have learned the use of simple state and complex state of useReducer and the use of multiple useReducer.

I have mastered the method of using useContext and useReducer to complete the modification of global state of child components when components are nested with multiple layers, which makes the code more elegant and has higher maintainability.

By comparing useState, learn how to use useEffect and useReducer to request data, and control the display and hiding of loading state.

Finally, useState and useReducer are compared, and suggestions are given.