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:
accumulator
The accumulator accumulates the return value of the callback; It is the cumulative value, or initialValue, that was returned when the callback was last called.currentValue
The 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:
useReducer
Is a hook API for state management.useReducer
Related to the Reducer functionuseReducer(reducer, initialState)
Accept two parameters, namely the reducer function and the initial statereducer(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
- import useReducer api
- Declare the Reducer function and initialState
- 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
- Create a counter method on the root node using useReducer
- 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
- 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.
- 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. - 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:
- The data is requested when the page loads
- The loading state is displayed in the request data
- 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.