Open React Hooks with animation and actual combat (3): useReducer and useContext

Posted May 25, 202016 min read

Open React Hooks with animation and actual combat(3):useReducer and useContext

This article is written by members of the Tuque community mRc , welcome to join tuque community Create wonderful free technical tutorials together to give the programming industry a boost.

If you think we have written well, remember like + follow + comment Sanlian, encourage us to write better tutorials ?

As application states become more complex, we urgently need solutions for state and data flow management. Students familiar with React development must have heard of Redux, and in this article, we will implement a simple version of Redux through the combination of useReducer + useContext. First of all, we will take you to re-recognize the "old friend" useState, and use this to lead the protagonist of this article:Reducer function and useReducer hook, and take you step by step through the basic ideas of data flow and state management.

useState:Liuminghuaming

Welcome to continue reading "Open the React Hooks series with animation and actual combat":

If you want to start learning directly from this article, then please clone the source code we provide for you:

git clone -b third-part https://github.com/tuture-dev/covid-19-with-hooks.git

# If your access to GitHub is not smooth, we also provide the Gitee address
git clone -b third-part https://gitee.com/tuture/covid-19-with-hooks.git

In this third article, we will first revisit useState. In the previous two tutorials, we can say that we have been fighting side by side with useState for a long time, and we are very old friends we are very familiar with. But looking back, do we really understand it enough?

An unresolved issue

You are likely to encounter a problem when using useState:when changing the state via Setter, how to read the last state value and modify it on this basis? If you read the documentation carefully enough, you should notice that useState has a usage of Functional Update(Functional Update), with the following counter(code from [React official website]( https://reactjs.org /docs/hooks-reference.html#functional-updates)) as an example:

function Counter({initialCount}) {
  const [count, setCount]= useState(initialCount);
  return(
    <>
      Count:{count}
      <button onClick = {() => setCount(initialCount)}> Reset </button>
      <button onClick = {() => setCount(prevCount => prevCount-1)}>-</button>
      <button onClick = {() => setCount(prevCount => prevCount + 1)}> + </button>
    </>
 );
}

It can be seen that what we pass in setCount is a function, its parameters are the previous state, and the new state is returned A friend familiar with Redux pointed out immediately:this is actually a Reducer function .

The past and present of Reducer function

The Reducer function has been explained in detail in the Redux documentation, but we will first return to the most basic concepts here and temporarily forget the knowledge about the framework. When learning the basics of JavaScript, you should have touched on the reduce method of arrays, which can sum arrays in a very cool way:

const nums = [1, 2, 3]
const value = nums.reduce((acc, next) => acc + next, 0)

The first parameter of reduce(acc, next) => acc + nextis a Reducer function. On the surface, this function accepts the accumulative valueacc of a state and the new value next, and then returns the updated accumulative value acc + next`. At a deeper level, the Reducer function has two necessary rules:

  • Only one value is returned
  • Do not modify the input value, but return the new value

The first point is easy to judge, and the second point is the pit that many novices have stepped on. Compare the following two functions:

//Not a Reducer function!
function buy(cart, thing) {
  cart.push(thing);
  return cart;
}

//Authentic Reducer function
function buy(cart, thing) {
  return cart.concat(thing);
}

The above function calls the array's push method, which will modify the input cart parameter(it does n t matter if return), which violates the second rule of Reducer, and the following function passes the array The concat method returned a new array, avoiding the direct modification of cart.

Let s look back at the previous functional update of useState:

setCount(prevCount => prevCount + 1);

Is it a standard Reducer?

The most familiar stranger

We used useState extensively in the first two tutorials. You might think that useState should be the bottommost element. But in fact, in the source code of React, useState is implemented using useReducer(the protagonist of this article, which will be described below). In React source code there is such a key function basicStateReducer Flow type definition in the source code):

function basicStateReducer(state, action) {
  return typeof action === 'function'? action(state):action;
}

So, when we change the state through setCount(prevCount => prevCount + 1), the action passed in is a Reducer function, then call the function and pass in the current state to get the updated state. When we previously modified the state by passing in specific values (such as setCount(5)), because it is not a function, we directly take the passed value as the updated state.

Tips

The source code of React v16.13.1 is selected here, but the overall implementation should have stabilized and will not differ much in principle .

Sounds a bit confused? It's our animation again. First, the action we pass in is a specific value:

When the Setter is passed to a Reducer function:

Is it suddenly bright?

There are many codes to be written in this step(you can copy and paste it yourself), we want to achieve the historical trend graph display effect shown in the following figure:

Note that we show three historical trends(Confirmed Cases, Deaths, and Recovered), and each historical trend graph can adjust the number of past days(from 0 to 30 days).

Realizing historical trend graph

First, let's implement the history curve HistoryChart component. Create src/components/HistoryChart.js component, the code is as follows:

//src/components/HistoryChart.js
import React from "react";
import {
  AreaChart,
  CartesianGrid,
  XAxis,
  YAxis,
  Tooltip,
  Area,
} from "recharts";

const TITLE2COLOR = {
  Cases:"# D0021B",
  Deaths:"# 4A4A4A",
  Recovered:"# 09C79C",
};

function HistoryChart({title, data, lastDays, onLastDaysChange}) {
  const colorKey = `color ${title}`;
  const color = TITLE2COLOR [title];

  return(
    <div>
      <AreaChart
        width = {400}
        height = {150}
        data = {data.slice(-lastDays)}
        margin = {{top:10, right:30, left:10, bottom:0}}
      >
        <defs>
          <linearGradient id = {colorKey} x1 = '0' y1 = '0' x2 = '0' y2 = '1'>
            <stop offset = '5%' stopColor = {color} stopOpacity = {0.8} />
            <stop offset = '95%'stopColor = {color} stopOpacity = {0} />
          </linearGradient>
        </defs>
        <XAxis dataKey = 'time' />
        <YAxis />
        <CartesianGrid strokeDasharray = '3 3' />
        <Tooltip />
        <Area
          type = 'monotone'
          dataKey = 'number'
          stroke = {color}
          fillOpacity = {1}
          fill = {`url(# ${colorKey})`}
        />
      </AreaChart>
      <h3> {title} </h3>
      <input
        type = 'range'
        min = '1'
        max = '30 '
        value = {lastDays}
        onChange = {onLastDaysChange}
      />
      Last {lastDays} days
    </div>
 );
}

export default HistoryChart;

Here we use the AreaChart component of Recharts to draw a historical trend chart, and then add below the chart A range drag bar allows users to choose to view the historical trend of the past 1 to 30 days.

The HistoryChart component contains the following Props:

  • title is the chart title
  • data is the historical data needed to draw the chart
  • lastDays is to display the data of the last N days, you can select it bydata.slice(-lastDays)
  • onLastDaysChange is the event processing function for the user to modify and process the past N days through input

Next, we need an auxiliary function to perform some conversion processing on the historical data. The historical data returned by NovelCOVID 19 API is an object:

{
  "3/28/20":81999,
  "3/29/20":82122
}

In order to be able to adapt to the data format of Recharts, we hope to convert to an array format:

[
  {
    time:"3/28/20",
    number:81999
  },
  {
    time:"3/29/20",
    number:82122
  }

]

This can be easily converted through Object.entries. We create the src/utils.js file and implement the transformHistory function. The code is as follows:

//src/utils.js
export function transformHistory(timeline = {}) {
  return Object.entries(timeline) .map((entry) => {
    const [time, number]= entry;
    return {time, number};
  });
}

Then we come to realize the historical trend chart group HistoryChartGroup, which contains three charts:confirmed cases(Cases), deaths(Deaths) and cured cases(Recovered). Create src/components/HistoryChartGroup.js, the code is as follows:

//src/components/HistoryChartGroup.js
import React, {useState} from "react";

import HistoryChart from "./HistoryChart";
import {transformHistory} from "../utils";

function HistoryChartGroup({history = {}}) {
  const [lastDays, setLastDays]= useState({
    cases:30,
    deaths:30,
    recovered:30,
  });

  function handleLastDaysChange(e, key) {
    setLastDays((prev) =>({... prev, [key]:e.target.value}));
  }

  return(
    <div className = 'history-group'>
      <HistoryChart
        title = 'Cases'
        data = {transformHistory(history.cases)}
        lastDays = {lastDays.cases}
        onLastDaysChange = {(e) => handleLastDaysChange(e, "cases")}
      />
      <HistoryChart
        title = 'Deaths'
        data = {transformHistory(history.deaths)}
        lastDays = {lastDays.deaths}
        onLastDaysChange = {(e) => handleLastDaysChange(e, "deaths")}
      />
      <HistoryChart
        title = 'Recovered'
        data = {transformHistory(history.recovered)}
        lastDays = {lastDays.recovered}
        onLastDaysChange = {(e) => handleLastDaysChange(e, "recovered")}
      />
    </div>
 );
}

export default HistoryChartGroup;

Adjust CountriesChart component

We need to adjust the CountriesChart component a little so that the user can display the corresponding historical trend graph after clicking on a country s data. Open src/components/CountriesChart.js, add an onClick Prop, and pass it into BarChart, as shown in the following code:

//src/components/CountriesChart.js
//...

function CountriesChart({data, dataKey, onClick}) {
  return(
    <BarChart
      width = {1200}
      height = {250}
      style = {{margin:"auto"}}
      margin = {{top:30, left:20, right:30}}
      data = {data}
      onClick = {onClick}
    >
      //...
    </BarChart>
 );
}

//...

Integration in the root component

Finally, we adjusted the root component to integrate the previously implemented historical trend graph and the modified CountriesChart into the application. Open src/App.js, the code is as follows:

//src/App.js
//...
import HistoryChartGroup from "./components/HistoryChartGroup";

function App() {
  //...

  const [country, setCountry]= useState(null);
  const history = useCoronaAPI(`/historical/${country}`, {
    initialData:{},
    converter:(data) => data.timeline,
  });

  return(
    <div className = 'App'>
      <h1> COVID-19 </h1>
      <GlobalStats stats = {globalStats} />
      <SelectDataKey onChange = {(e) => setKey(e.target.value)} />
      <CountriesChart
        data = {countries}
        dataKey = {key}
        onClick = {(payload) => setCountry(payload.activeLabel)}
      />

      {country?(
        <>
          <h2> History for {country} </h2>
          <HistoryChartGroup history = {history} />
        </>
     ):(
        <h2> Click on a country to show its history. </h2>
     )}
    </div>
 );
}

export default App;

Success

Open the project after writing, click on any country in the histogram, it will display the historical trend chart of the country(cumulative diagnosis, death cases, cured cases), we can also adjust the number of days in the past.

Although our application has taken shape initially, but looking back at the code, we found that the logic of the state of the component and the logic of modifying the state are scattered in each component. Undoubtedly, we will encounter great difficulties in maintaining and implementing new functions later Need to do special state management. Students familiar with React development must know libraries like Redux or MobX , but with the help of React Hooks, We can easily implement a lightweight state management solution ourselves.

useReducer + useContext:the wind and the rain

As we said before, this article will use React Hooks to implement a lightweight Redux-like state management model. But before that, let's briefly go through the basic idea of Redux(familiar students can skip it directly).

Redux basic idea

Previously, the state of the application(such as the current country in our application, historical data, etc.) was scattered in various components, probably like this:

As you can see, each component has its own State and State Setter, which means that reading and modifying state across components is quite troublesome. One of the core ideas of Redux is to put the state into the only global object(generally called Store), and to modify the state is to call the corresponding Reducer function to update the state in the Store, probably like this:

The above animation describes the process of component A changing state in B and C:

  • When the three components are mounted, get and subscribe corresponding status data from the Store and display it(note that it is read-only and cannot be modified directly)
  • User clicks on component A to trigger event monitoring function
  • Dispatch(Dispatch) corresponding action(Action) in the monitoring function, pass in the Reducer function
  • The Reducer function returns the updated state and updates the Store based on this
  • Since components B and C subscribe to the state of the store, reacquire the updated state and adjust the UI

Tips

This tutorial will not explain Redux in detail. Students who want to study in depth can read our "Redux package teaching package" series of tutorials.

useReducer

First, let's take a look at the official use of useReducer:

const [state, dispatch]= useReducer(reducer, initialArg, init);

First let's take a look at what parameters useReducer needs to provide:

  1. The first parameter reducer is obviously necessary, and its form is exactly the same as the Reducer function in Redux, that is,(state, action) => newState.
  2. The second parameter initialArg is the initial value of the state.
  3. The third parameter init is an optional function for Lazy Initialization, which returns the state after initialization.

The returned state(read-only state) and dispatch(dispatch function) are easier to understand. Let's explain it with a simple counter example:

//Reducer function
function reducer(state, action) {
  switch(action.type) {
    case 'increment':
      return {count:state.count + 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch]= useReducer(reducer, {count:0});
  return(
    <>
      Count:{state.count}
      <button onClick = {() => dispatch({type:'increment'})}> + </button>
    </>
 );
}

Let's first focus on the Reducer function. Its two parameters state and action are the current state and the action dispatched by dispatch. The actions here are ordinary JavaScript objects, which are used to indicate operations that modify the state. Note that type is a required attribute and represents the type of action. Then we return the corresponding modified new state according to the type of action.

Then in the Counter component, we get the state and the dispatch function through the useReducer hook, and then render this state. In the onClick callback function of the button button, we use dispatch an action of type increment to update the status.

Oh my god, why a simple counter is so complicated! Is n t it easy to just use useState?

When should I use useReducer

You may find that useReducer and useState are used for almost the same purpose:define state and modify state logic. useReducer is cumbersome to use, but if your state management has at least one of the problems listed below:

  • The states that need to be maintained are more complex, and multiple states depend on each other
  • The process of modifying the state is more complicated

Then we strongly recommend that you use useReducer. Let's feel the power of useReducer through a practical case(this time it is not a boring counter). Suppose we want to make an editor that supports Undo and Redo, and its init function and Reducer function are as follows:

//function for lazy initialization
function init(initialState) {
  return {
    past:[],
    present:initialState,
    future:[],
  };
}

//Reducer function
function reducer(state, action) {
  const {past, future, present} = state;
  switch(action.type) {
    case 'UNDO':
      return {
        past:past.slice(0, past.length-1),
        present:past [past.length-1],
        future:[present, ... future],
      };
    case 'REDO':
      return {
        past:[... past, present],
        present:future [0],
        future:future.slice(1),
      };
    default:
      return state;
  }
}

Try using useState to write, will it be complicated?

useContext

Now that the acquisition and modification of the state have been done through useReducer, then there is only one problem:how can all components get the dispatch function?

Before the birth of Hooks, React had a solution to share data in the component tree: Context . In the class component, we can get the nearest Context Provider through the Class.contextType attribute, so in the functional component, how do we get it? The answer is the useContext hook. It is very simple to use:

//Define MyContext in a file
const MyContext = React.createContext('hello');

//Get Context in functional component
function Component() {
  const value = useContext(MyContext);
  //...
}

Through useContext, we can easily make all components get the dispatch function!

Design Center Status

Okay, let's start using the combination of useReducer + useContext to reconstruct the state management of the application. According to the principle of state centralization, we extract the state of the entire application into a global object. The preliminary design(TypeScript type definition) is as follows:

type AppState {
  //Data indicator category
  key:"cases" | "deaths" | "recovered";

  //current country
  country:string | null;

  //past days
  lastDays:{
    cases:number;
    deaths:number;
    recovered:number;
  }
}

Define Reducer and Dispatch Context in the root component

This time, in the order of top-down, we first configure all the required Reducer and Dispatch context in the root component App. Open src/App.js and modify the code as follows:

//src/App.js
import React, {useReducer} from "react";

//...

const initialState = {
  key:"cases",
  country:null,
  lastDays:{
    cases:30,
    deaths:30,
    recovered:30,
  },
};

function reducer(state, action) {
  switch(action.type) {
    case "SET_KEY":
      return {... state, key:action.key};
    case "SET_COUNTRY":
      return {... state, country:action.country};
    case "SET_LASTDAYS":
      return {
        ... state,
        lastDays:{... state.lastDays, [action.key]:action.days},
      };
    default:
      return state;
  }
}

//React Context for delivering dispatch
export const AppDispatch = React.createContext(null);

function App() {
  const [state, dispatch]= useReducer(reducer, initialState);
  const {key, country, lastDays} = state;

  const globalStats = useCoronaAPI("/all", {
    initialData:{},
    refetchInterval:5000,
  });

  const countries = useCoronaAPI(`/countries? sort = ${key}`, {
    initialData:[],
    converter:(data) => data.slice(0, 10),
  });

  const history = useCoronaAPI(`/historical/${country}`, {
    initialData:{},
    converter:(data) => data.timeline,
  });

  return(
    <AppDispatch.Provider value = {dispatch}>
      <div className = 'App'>
        <h1> COVID-19 </h1>
        <GlobalStats stats = {globalStats} />
        <SelectDataKey />
        <CountriesChart data = {countries} dataKey = {key} />

        {country?(
          <>
            <h2> History for {country} </h2>
            <HistoryChartGroup history = {history} lastDays = {lastDays} />
          </>
       ):(
          <h2> Click on a country to show its history. </h2>
       )}
      </div>
    </AppDispatch.Provider>
 );
}

export default App;

Let's analyze the above code changes one by one:

  1. First define the initial state of the entire application initialState, which is needed by the useReducer hook later
  2. Then we define the Reducer function, which mainly responds to three actions:SET_KEY, SET_COUNTRY and SET_LASTDAYS, which are used to modify the three states of data indicators, countries and past days, respectively
  3. Define the AppDispatch Context, which is used to pass dispatch to the child components
  4. Call the useReducer hook to get the state state and the dispatch function dispatch
  5. Finally, use AppDispatch.Provider to wrap the entire application during rendering and pass in dispatch so that the child components can be obtained

Modify state via Dispatch in subcomponents

Now all the state of the sub-component has been extracted into the root component, and the only thing the sub-component has to do is to modify the central state through dispatch in response to user events. The idea is very simple:

  • First use useContext to get the dispatch passed down by the App component
  • Call dispatch to initiate the corresponding action(Action)

OK, let's get started. Open src/components/CountriesChart.js and modify the code as follows:

//src/components/CountriesChart.js
import React, {useContext} from "react";
//...
import {AppDispatch} from "../App";

function CountriesChart({data, dataKey}) {
  const dispatch = useContext(AppDispatch);

  function onClick(payload = {}) {
    if(payload.activeLabel) {
      dispatch({type:"SET_COUNTRY", country:payload.activeLabel});
    }
  }

  return(
    //...
 );
}

export default CountriesChart;

Following the same idea, let's modify the src/components/HistoryChartGroup.js component:

//src/components/HistoryChartGroup.js
import React, {useContext} from "react";

import HistoryChart from "./HistoryChart";
import {transformHistory} from "../utils";
import {AppDispatch} from "../App";

function HistoryChartGroup({history = {}, lastDays = {}}) {
  const dispatch = useContext(AppDispatch);

  function handleLastDaysChange(e, key) {
    dispatch({type:"SET_LASTDAYS", key, days:e.target.value});
  }

  return(
    //...
 );
}

export default HistoryChartGroup;

Last mile, modify src/components/SelectDataKey.js:

//src/components/SelectDataKey.js
import React, {useContext} from "react";
import {AppDispatch} from "../App";

function SelectDataKey() {
  const dispatch = useContext(AppDispatch);

  function onChange(e) {
    dispatch({type:"SET_KEY", key:e.target.value});
  }

  return(
    //...
 );
}

export default SelectDataKey;

After the refactoring is completed, running the project, you should find that it is the same as the previous step.

Tips

If you are familiar with Redux, you will find that there is a small regret in our refactoring:subcomponents can only obtain the state in the root component App by passing Props. A workaround is to solve the problem by putting state into the Context, but if you encounter this kind of demand, I still recommend using Redux directly.

Is Redux still useful:the battle between Control and Context

I heard some voices saying that with React Hooks, Redux is no longer needed. Is Redux still useful?

Before answering this question, please allow me to think about it. React Hooks are indeed terribly powerful, especially through the excellent third-party custom Hooks library, which allows almost every component to handle complex business logic with ease. In contrast to Redux, its core idea is to centralize the operations of the state and modify the state.

Have you found that this actually corresponds to two management thoughts Context and Control?

Managers need Context, not Control. Zhang Yiming, founder and CEO of ByteDance

Control is to centralize power, employees only need to orderly perform the corresponding tasks in accordance with the CEO's decision, just like the global store in Redux is "the only source of truth"(Single Source of Truth), all status and data flow updates Must go through the store; and Context is to give each department and all levels sufficient decision-making power, because they have more context, professionalism is better, just like the components in React that respond to specific logic Have a more sufficient context, and can use Hooks to "self-sufficient" to perform tasks without relying on the global Store.

Having talked here, I think you already have your own answer in your heart. _If you want to share, please leave a message in the comment area ~ _

References

Want to learn more exciting practical technical tutorials? Come and visit Tuque Community .