【Hooks】:[組]How to useReducer in React

2021-03-02 WebJ2EE

目錄Part 1: What is a reducer in JavaScript?Part 2: How to useReducer in React?  2.1. Reducer in React  2.2. React's useReducer HookPart 3: What about useState? Can’t we use that instead?

Part 1: What is a reducer in JavaScript?

The concept of a Reducer became popular in JavaScript with the rise of Redux as state management solution for React. But no worries, you don't need to learn Redux to understand Reducers. Basically reducers are there to manage state in an application. For instance, if a user writes something in an HTML input field, the application has to manage this UI state (e.g. controlled components).

(state, action) => newState

As example, it would look like the following in JavaScript for the scenario of increasing a number by one:

function counterReducer(state, action) {  return state + 1;}

Or defined as JavaScript arrow function, it would look the following way for the same logic:

const counterReducer = (state, action) => {  return state + 1;};

In this case, the current state is an integer (e.g. count) and the reducer function increases the count by one. If we would rename the argument state to count, it may be more readable and approachable by newcomers to this concept. However, keep in mind that the count is still the state:

const counterReducer = (count, action) => {  return count + 1;};

The reducer function is a pure function without any side-effects, which means that given the same input (e.g. state and action), the expected output (e.g. newState) will always be the same. This makes reducer functions the perfect fit for reasoning about state changes and testing them in isolation. You can repeat the same test with the same input as arguments and always expect the same output:

expect(counterReducer(0)).to.equal(1); // successful testexpect(counterReducer(0)).to.equal(1); // successful test

That's the essence of a reducer function. However, we didn't touch the second argument of a reducer yet: the action. The action is normally defined as an object with a type property. Based on the type of the action, the reducer can perform conditional state transitions:

const counterReducer = (count, action) => {  if (action.type === 'INCREASE') {    return count + 1;  }
if (action.type === 'DECREASE') { return count - 1; }
return count;};

If the action type doesn't match any condition, we return the unchanged state. Testing a reducer function with multiple state transitions -- given the same input, it will always return the same expected output -- still holds true as mentioned before which is demonstrated in the following test cases:

// successful tests// because given the same input we can always expect the same outputexpect(counterReducer(0, { type: 'INCREASE' })).to.equal(1);expect(counterReducer(0, { type: 'INCREASE' })).to.equal(1);
// other state transitionexpect(counterReducer(0, { type: 'DECREASE' })).to.equal(-1);
// if an unmatching action type is defined the current state is returnedexpect(counterReducer(0, { type: 'UNMATCHING_ACTION' })).to.equal(0);

However, more likely you will see a switch case statement in favor of if else statements in order to map multiple state transitions for a reducer function. The following reducer performs the same logic as before but expressed with a switch case statement:

const counterReducer = (count, action) => {  switch (action.type) {    case 'INCREASE':      return count + 1;    case 'DECREASE':      return count - 1;    default:      return count;  }};

In this scenario, the count itself is the state on which we are applying our state changes upon by increasing or decreasing the count. However, often you will not have a JavaScript primitive (e.g. integer for count) as state, but a complex JavaScript object. For instance, the count could be one property of our state object:

const counterReducer = (state, action) => {  switch (action.type) {    case 'INCREASE':      return { ...state, count: state.count + 1 };    case 'DECREASE':      return { ...state, count: state.count - 1 };    default:      return state;  }};

Don't worry if you don't understand immediately what's happening in the code here. Foremost, there are two important things to understand in general:

The state processed by a reducer function is immutable.That means the incoming state -- coming in as argument -- is never directly changed. Therefore the reducer function always has to return a new state object. If you haven't heard about immutability, you may want to check out the topic immutable data structures.

Since we know about the state being a immutable data structure, we can use the JavaScript spread operator to create a new state object from the incoming state and the part we want to change (e.g. count property). This way we ensure that the other properties that aren't touch from the incoming state object are still kept intact for the new state object.

Let's see these two important points in code with another example where we want to change the last name of a person object with the following reducer function:

const personReducer = (person, action) => {  switch (action.type) {    case 'INCREASE_AGE':      return { ...person, age: person.age + 1 };    case 'CHANGE_LASTNAME':      return { ...person, lastname: action.lastname };    default:      return person;  }};

We could change the last name of a user the following way in a test environment:

const initialState = {  firstname: 'Liesa',  lastname: 'Huppertz',  age: 30,};
const action = { type: 'CHANGE_LASTNAME', lastname: 'Wieruch',};
const result = personReducer(initialState, action);
expect(result).to.equal({ firstname: 'Liesa', lastname: 'Wieruch', age: 30,});

You have seen that by using the JavaScript spread operator in our reducer function, we use all the properties from the current state object for the new state object but override specific properties (e.g. lastname) for this new object. That's why you will often see the spread operator for keeping state operation immutable (= state is not changed directly).

Also you have seen another aspect of a reducer function: An action provided for a reducer function can have an optional payload (e.g. lastname) next to the mandatory action type property.The payload is additional information to perform the state transition. For instance, in our example the reducer wouldn't know the new last name of our person without the extra information.

Often the optional payload of an action is put into another generic payload property to keep the top-level of properties of an action object more general (.e.g { type, payload }). That's useful for having type and payload always separated side by side. For our previous code example, it would change the action into the following:

const action = {  type: 'CHANGE_LASTNAME',  payload: {    lastname: 'Wieruch',  },};

The reducer function would have to change too, because it has to dive one level deeper into the action:

const personReducer = (person, action) => {  switch (action.type) {    case 'INCREASE_AGE':      return { ...person, age: person.age + 1 };    case 'CHANGE_LASTNAME':      return { ...person, lastname: action.payload.lastname };    default:      return person;  }};

Basically you have learned everything you need to know for reducers. They are used to perform state transitions from A to B with the help of actions that provide additional information. You can find reducer examples from this tutorial in this GitHub repository including tests. Here again everything in a nutshell:

Syntax: In essence a reducer function is expressed as (state, action) => newState.

Immutability: State is never changed directly. Instead the reducer always creates a new state.

State Transitions: A reducer can have conditional state transitions.

Action: A common action object comes with a mandatory type property and an optional payload:

Part 2: How to useReducer in React?

Since React Hooks have been released, function components can use state and side-effects. There are two hooks that are used for modern state management in React: useState and useReducer. This tutorial goes step by step through a useReducer example in React for getting you started with this React Hook for state management.

2.1. Reducer in React

The following function is a reducer function for managing state transitions for a list of items:

const todoReducer = (state, action) => {  switch (action.type) {    case 'DO_TODO':      return state.map(todo => {        if (todo.id === action.id) {          return { ...todo, complete: true };        } else {          return todo;        }      });    case 'UNDO_TODO':      return state.map(todo => {        if (todo.id === action.id) {          return { ...todo, complete: false };        } else {          return todo;        }      });    default:      return state;  }};

There are two types of actions for an equivalent of two state transitions. They are used to toggle the complete boolean to true or false of a todo item. As additional payload an identifier is needed which coming from the incoming action's payload.

The state which is managed in this reducer is an array of items:

const todos = [  {    id: 'a',    task: 'Learn React',    complete: false,  },  {    id: 'b',    task: 'Learn Firebase',    complete: false,  },];
const action = { type: 'DO_TODO', id: 'a',};
const newTodos = todoReducer(todos, action);
console.log(newTodos);// [// {// id: 'a',// task: 'Learn React',// complete: true,// },// {// id: 'b',// task: 'Learn Firebase',// complete: false,// },// ]

So far, everything demonstrated here is not related to React. If you have any difficulties to understand the reducer concept, please revisit the referenced tutorial from the beginning for Reducers in JavaScript. Now, let's dive into React's useReducer hook to integrate reducers in React step by step.

2.2. React's useReducer Hook

The useReducer hook is used for complex state and state transitions. It takes a reducer function and an initial state as input and returns the current state and a dispatch function as output with array destructuring:

const initialTodos = [  {    id: 'a',    task: 'Learn React',    complete: false,  },  {    id: 'b',    task: 'Learn Firebase',    complete: false,  },];
const todoReducer = (state, action) => { switch (action.type) { case 'DO_TODO': return state.map(todo => { if (todo.id === action.id) { return { ...todo, complete: true }; } else { return todo; } }); case 'UNDO_TODO': return state.map(todo => { if (todo.id === action.id) { return { ...todo, complete: false }; } else { return todo; } }); default: return state; }};
const [todos, dispatch] = useReducer(todoReducer, initialTodos);

The dispatch function can be used to send an action to the reducer which would implicitly change the current state:

const [todos, dispatch] = React.useReducer(todoReducer, initialTodos);
dispatch({ type: 'DO_TODO', id: 'a' });

The previous example wouldn't work without being executed in a React component, but it demonstrates how the state can be changed by dispatching an action. Let's see how this would look like in a React component. We will start with a React component rendering a list of items. Each item has a checkbox as controlled component:

import React from 'react';
const initialTodos = [ { id: 'a', task: 'Learn React', complete: false, }, { id: 'b', task: 'Learn Firebase', complete: false, },];
const App = () => { const handleChange = () => {};
return ( <ul> {initialTodos.map(todo => ( <li key={todo.id}> <label> <input type="checkbox" checked={todo.complete} onChange={handleChange} /> {todo.task} </label> </li> ))} </ul> );};
export default App;

It's not possible to change the state of an item with the handler function yet. However, before we can do so, we need to make the list of items stateful by using them as initial state for our useReducer hook with the previously defined reducer function:

import React from 'react';
const initialTodos = [...];
const todoReducer = (state, action) => { switch (action.type) { case 'DO_TODO': return state.map(todo => { if (todo.id === action.id) { return { ...todo, complete: true }; } else { return todo; } }); case 'UNDO_TODO': return state.map(todo => { if (todo.id === action.id) { return { ...todo, complete: false }; } else { return todo; } }); default: return state; }};
const App = () => { const [todos, dispatch] = React.useReducer( todoReducer, initialTodos );
const handleChange = () => {};
return ( <ul> {todos.map(todo => ( <li key={todo.id}> ... </li> ))} </ul> );};
export default App;

Now we can use the handler to dispatch an action for our reducer function. Since we need the id as the identifier of a todo item in order to toggle its complete flag, we can pass the item within the handler function by using a encapsulating arrow function:

const App = () => {  const [todos, dispatch] = React.useReducer(    todoReducer,    initialTodos  );
const handleChange = todo => { dispatch({ type: 'DO_TODO', id: todo.id }); };
return ( <ul> {todos.map(todo => ( <li key={todo.id}> <label> <input type="checkbox" checked={todo.complete} onChange={() => handleChange(todo)} /> {todo.task} </label> </li> ))} </ul> );};

This implementation works only one way though: Todo items can be completed, but the operation cannot be reversed by using our reducer's second state transition. Let's implement this behavior in our handler by checking whether a todo item is completed or not:

const App = () => {  const [todos, dispatch] = React.useReducer(    todoReducer,    initialTodos  );
const handleChange = todo => { dispatch({ type: todo.complete ? 'UNDO_TODO' : 'DO_TODO', id: todo.id, }); };
return ( <ul> {todos.map(todo => ( <li key={todo.id}> <label> <input type="checkbox" checked={todo.complete} onChange={() => handleChange(todo)} /> {todo.task} </label> </li> ))} </ul> );};

Depending on the state of our todo item, the correct action is dispatched for our reducer function. Afterward, the React component is rendered again but using the new state from the useReducer hook. 

React's useReducer hook is a powerful way to manage state in React. It can be used with useState and useContext for modern state management in React. Also, it is often used in favor of useState for complex state and state transitions. After all, the useReducer hook hits the sweet spot for middle sized applications that don't need Redux for React yet.

Part 3: What about useState? Can’t we use that instead?

An astute reader may have been asking this all along. I mean, setState is generally the same thing, right? Return a stateful value and a function to re-render a component with that new value.

const [state, setState] = useState(initialState);

We could have even used the useState() hook in the counter example provided by the React docs. However, useReducer is preferred in cases where state has to go through complicated transitions. Kent C. Dodds wrote up a explanation of the differences between the two and (while he often reaches for setState) he provides a good use case for using useReducer instead:

My rule of thumb is to reach for useReducer to handle complex states, particularly where the initial state is based on the state of other elements.

參考:

What is a Reducer in JavaScript/React/Redux?

https://www.robinwieruch.de/javascript-reducer/

How to useReducer in React:

https://www.robinwieruch.de/react-usereducer-hook

Getting to Know the useReducer React Hook:

https://css-tricks.com/getting-to-know-the-usereducer-react-hook/

Should I useState or useReducer?

https://kentcdodds.com/blog/should-i-usestate-or-usereducer

相關焦點

  • 【Hooks】:[組]Awesome React Hooks
    Let’s look at how we could implement this custom Hook./state-management-with-react-hooks-no-redux-or-context-api-8b3035ceecf8How to fetch data with React Hooks?
  • 【Hooks】:[組]Declarative & Imperative
    "But why is this a 'more accurate implementation of the react mental model'?"React hooks allow components to be truly declarative even if they contain state and side-effects.
  • 牛津大學- 如何使用感嘆詞 How to Use Interjections in English
    In this lesson, you can learn about how to use interjections in English.What are interjections?本節課,你會學習關於如何在英語中使用感嘆詞。
  • 我讀完了React的API,並為新手送上了一些建議
    一個 hook 以「use」作為前綴來聲明,比如 useState。它返回狀態和一個 setter(如果需要),或僅返回狀態。const [count, setCount] = useState(0)我們初始化了 count = 0。我們包含了一個用於更新狀態的 setter。我們不應該在組件中執行 count +=1,因為它們會引入錯誤。
  • 2019 React Redux 完全指南
    搜索 redux 添加依賴,然後再次點擊 Add Dependency 搜索 react-redux 添加。在本地項目,你可以通過 Yarn 或者 NPM 安裝:npm install —save redux react-redux。
  • React 靈魂 23 問,你能答對幾個?
    :2、聊聊 react@16.4 + 的生命周期相關連接:React 生命周期 我對 React v16.4 生命周期的理解3、useEffect(fn, []) 和 componentDidMount 有什麼差異?useEffect 會捕獲 props 和 state。
  • 精通react/vue組件設計之配合React Portals實現一個(Drawer)組件
    通過組件的設計過程,大家會接觸到一個完成健壯的組件設計思路和方法,也能在實現組件的過程逐漸對react/vue的高級知識和技巧有更深的理解和掌握,並且在企業實際工作做遊刃有餘.作為數據驅動的領導者react/vue等MVVM框架的出現,幫我們減少了工作中大量的冗餘代碼, 一切皆組件的思想深得人心.
  • 分享7 個很棒的 React Hooks,內含它們的用法和示例
    第三個:useFetch HookSteven Persia(一名MERN Stack開發人員)編寫了一份名為「 Captain hook 」 的React Hooks清單,這對日常工作非常有幫助。Hooks的以下幾個示例來自他的收藏。useFetch可用於從API獲取數據。請求完成後,它將返迴響應和錯誤(如果有)。
  • 使用React、Electron、Dva、Webpack、Node.js、Websocket快速構建跨平臺應用
    /index.html'], vendor: ['react']        }忽略Electron中的代碼,不用webpack打包(因為Electron中有後臺模塊代碼,打包就會報錯)externals: [ (function () {
  • React詳解--react之redux
    首先,我們要理解 Redux 幾個核心概念與它們之間的關係:○store○state○action○reducerStoreStore就是保存數據的地方,你可以把它看成一個容器。整個應用只能有一個Store。
  • Vue3 & React Hooks 新UI組件原理:Modal 彈窗
    步驟一:創建一個Modal組件步驟二:自定義useModal「參考文章:」❝《Building a simple and reusable modal with React hooks and portals》《Portal – a new feature in Vue 3》覺得本文對你有幫助?
  • 【英】新版 React DevTools 簡介
    react-domreact-nativeHow do I get the new DevTools?React DevTools is available as an extension for Chrome and Firefox.
  • 你真的會用 ACTUALLY 嗎 How To Use ACTUALLY
    different positions within an English sentence as well so that can make it seem a little confusing and with many of my students, if you know, they're a little confused about a word or a little unsure about how
  • 牛津大學- 如何用英語表達諷刺 How to Use Sarcasm in English
    In this lesson, you can learn about how to be sarcastic in English.Imagine your friend has an exam.本節課,你可以學習如何用英語表達諷刺。
  • 如何使用英語縮寫 How to Say & Use English Abbreviations
    To get all the details about how the Sprint works and how you can get a one hundred percent refund on your class fees, then make sure you check out this video up here that has all the
  • React Native 0.62 發布,默認支持 Flipper,新的暗黑模式
    colorScheme = Appearance.getColorScheme();if (colorScheme === 'dark') { // Use dark color scheme}同時還添加了一個 hook 跟蹤用戶首選項的狀態更新:import {Text, useColorScheme
  • Chinese 2nd grader makes animated video on how to use mask...
    Chinese 2nd grader makes animated video on how to use mask amid fight against virus 大字 日期:2020-03-02 來源:新華社
  • 前端大神用React刻了一個Windows XP
    為了完全模擬踩地雷的行為,將 mouse event 的 button 和 buttons 屬性組合了一下才完成,複雜的狀態則使用 useReducer 來管理。https://bitsofco.de/how-display-contents-works/http://www.colorzilla.com/gradient-editor/https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttonsGitHub:https://github.com
  • 《精通react/vue組件設計》之實現一個健壯的警告提示(Alert)組件
    選手來說,如果沒用typescript,建議大家都用PropTypes, 它是react內置的類型檢測工具,我們可以直接在項目中導入. vue有自帶的屬性檢測方式,這裡就不一一介紹了.基於react實現一個Alert組件2.1.