This article is written by mRc, a member of the Tuquai community. Welcome to join tuquai community and create wonderful free technical tutorials together to help the development of the programming industry.

If you think we wrote well, remember to like + follow + comment three times, encourage us to write a better tutorial 💪

As application state becomes more and more complex, we urgently need state and data flow management solutions. If you are familiar with React development, you will have heard of Redux. In this article, we will implement a simplified version of Redux using a combination of useReducer and useContext. First of all, we will introduce you to the “old friend” useState, and draw the main role of this article: Reducer function and useReducer hook, and take you step by step to clarify the basic ideas of data flow and state management through actual practice.

UseState: A silver lining

Keep reading on the React Hooks series:

  • React-hooks (1) : useState and useEffect
  • React-hooks: Custom Hooks and useCallback

If you would like to start with this article directly, please clone the source code we have provided for you:

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

# If you don't have smooth access to GitHub, we also provide a Gitee address
git clone -b third-part https://gitee.com/tuture/covid-19-with-hooks.git
Copy the code

In this third article, we’ll first revisit useState. In the previous two tutorials, we can say that we have been working with useState for a long time and are very “familiar” friends. But looking back, do we really know enough about it?

An unsolved problem

You’ve probably run into a problem with useState: how do you read the last state value and change it from there when you use setters to change the state? If you read the documentation carefully enough, you’ll notice that useState has a Functional Update. Use the React counter 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>
    </>
  );
}
Copy the code

As you can see, setCount is a function that takes the previous state and returns the new state. Friends familiar with Redux are quick to point out that this is really a Reducer function.

Reducer function past life and present life

The Reducer function has been described in detail in the Redux document, but we will first go back to the basic concepts and temporarily forget the knowledge related to the framework. While learning the basics of JavaScript, you should have been exposed to the reduce method for arrays, which implements array summing in a pretty cool way:

const nums = [1.2.3]
const value = nums.reduce((acc, next) = > acc + next, 0)
Copy the code

The first parameter of reduce (ACC, next) => ACC + next is a Reducer function. On the face of it, this function takes a cumulative value of the state acc and the new value next, and returns the updated cumulative value ACC + Next. At a deeper level, the Reducer function has two necessary rules:

  • Only one value is returned
  • Instead of modifying the input value, the new value is returned

The first one is easy to judge, and the second one is a pit that many beginners step on. Compare the following two functions:

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

// An authentic Reducer function
function buy(cart, thing) {
  return cart.concat(thing);
}
Copy the code

The above function calls the push method of the array, modifying the input cart parameters in place (it doesn’t matter whether return is required or not) and violates the second Reducer rule, while the following function returns a new array through the concat method of the array, avoiding direct modification of the CART.

Let’s go back to the previous functional update to useState:

setCount(prevCount= > prevCount + 1);
Copy the code

Is this a standard Reducer?

The most familiar stranger

We’ve used useState extensively in the last two tutorials, and you might think that useState would be the lowest element. However, in the React source code, the useState implementation uses the useReducer. BasicStateReducer function basicStateReducer function basicStateReducer function basicStateReducer function basicStateReducer function basicStateReducer

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

Thus, when we change the state with setCount(prevCount => prevCount + 1), the action passed in is a Reducer function, which is then called and passed in the current state to get the updated state. When we changed the state by passing in a specific value (such as setCount(5)), we simply took the value as the updated state because it was not a function.

prompt

The React V16.13.1 source code is used here, but the overall implementation should be stable and the principle should not be too different.

Still sound a little foggy? It’s time for our animation. First, the action we pass in is a concrete value:

When the Setter is passed in a Reducer function:

Did it all fall into place all of a sudden?

Practical link

This step is a lot of code to write (you can copy and paste), we want to achieve the following historical trend chart display effect:

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

Implement historical trend charts

First, let’s implement the HistoryChart component. Create a SRC/components/HistoryChart js components, 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;Copy the code

Here we used the AreaChart component of Recharts to chart historical trends and then added a range drag bar at the bottom of the chart to give users the option to view historical trends for the past 1 to 30 days.

The HistoryChart component contains the following Props:

  • titleIs the title of the chart
  • dataThat’s the historical data you need to make a graph
  • lastDaysYes Displays the data of the last N daysdata.slice(-lastDays)Make a selection
  • onLastDaysChangeYes, the userinputModify the event handler for the past N days

Next, we need an auxiliary function to do some transformation of the historical data. The historical data returned by the NovelCOVID 19 API is an object:

{
  "3/28/20": 81999."3/29/20": 82122
}
Copy the code

To accommodate the data format of Recharts, we want to convert to array format:

[{time: "3/28/20".number: 81999
  },
  {
    time: "3/29/20".number: 82122}]Copy the code

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

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

The HistoryChartGroup consists of three charts: Cases, Deaths and Recovered Cases. Create a SRC/components/HistoryChartGroup js, HTML 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;
Copy the code

Adjust CountriesChart component

We needed to tweak the CountriesChart component a little bit so that when the user clicks on a country’s data, he can display the corresponding historical trend chart. Open the SRC/components/CountriesChart js, add an onClick Prop, and the incoming BarChart, shown in the code below:

// 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>
  );
}

// ...
Copy the code

Integrate in the root component

Finally, we adjusted the root component and integrated the historical trend chart previously implemented with the modified CountriesChart into the application. Open SRC/app.js with the following code:

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

successful

After writing, open the project and click on any country in the histogram to show the country’s historical trends (cumulative confirmed cases, deaths, cured cases) and adjust the number of days that have gone by.

Although our application has taken initial shape now, when we look back at the code, we find that the state of the component and the logic to modify the state are scattered in each component. It will undoubtedly be very difficult to maintain and implement new functions in the future. At this time, we need to do special state management. Those of you familiar with React development 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: Call the wind and call the rain

As we said earlier, this article will implement a lightweight, Redux-like state management model using React Hooks. But before that, let’s go over the basic idea of Redux (if you’re familiar with it, skip it).

Redux’s basic idea

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

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

The animation above shows component A changing state in B and C:

  • When the three components are mounted, the corresponding state data is fetched and subscribed from the Store and displayed (note that it is read-only and cannot be modified directly).
  • The user clicks component A to trigger the event listener function
  • The actions corresponding to Dispatch in the listener function are passed into the Reducer function
  • The Reducer function returns the updated state and updates the Store with it
  • Since components B and C subscribe to the Store state, they re-fetch the updated state and adjust the UI

prompt

This tutorial will not cover Redux in detail, but if you want to learn more, you can read our Redux Tutorial series.

Use of useReducer

First of all, let’s take a look at the official introduction of the use of useReducer:

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

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

  1. The first parameterreducerIt is obviously necessary, and its form is exactly the same as the Reducer function in Redux, i.e(state, action) => newState.
  2. Second parameterinitialArgThat’s the initial value of the state.
  3. The third parameterinitIs an optional forLazy initializationFunction of (Lazy Initialization), which returns the state after Initialization.

The returned state (read-only state) and Dispatch (dispatch function) are easier to understand. Let’s combine this 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>
    </>
  );
}
Copy the code

Let’s first focus on the Reducer function, whose two parameters, state and action, are the current state and the actions dispatched by Dispatch, respectively. The action here is a normal JavaScript object that represents a state modification operation. Note that type is a mandatory property that represents the type of the action. We then return the modified new state based on the action type.

Then in the Counter component, we get the state and the dispatch function via the useReducer hook and render the state. In button’s onClick callback, we update the status by dispatching an Action of type INCREMENT.

God, how could a simple counter be so complicated! Why not just a useState?

When should YOU use useReducer

You may find that useReducer and useState serve almost the same purpose: the logic for defining and modifying states. UseReducer can be tedious to use, but if your state management has at least one of the following problems:

  • The states that need to be maintained are complex and depend on each other
  • The process of changing the status is complicated

Then we strongly recommend that you use useReducer. Let’s get a feel for the power of useReducer with a practical example (this time 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:
      returnstate; }}Copy the code

Try using useState. Is it complicated?

Use useContext

Now that the state has been obtained and modified by the useReducer, there is only one question left: how can all components obtain the dispatch function?

React had a solution for sharing data in a component tree before Hooks came along: Context. In a Class component, we can get the latest Context Provider using the class.contextType property. How do we get the latest Context Provider in a functional component? The answer is the useContext hook. It’s very simple to use:

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

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

With useContext, we can easily make the Dispatch function available to all components!

Practical link

Design center state

Ok, let’s start refactoring the application’s state management using a combination of useReducer and useContext. Following the principle of state centralization, we extract the state of the entire application into a global object. The initial 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; }}Copy the code

Define the Reducer and Dispatch contexts in the root component

This time, we first configured all required Reducer and Dispatch contexts in the root component App in a top-down order. 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:
      returnstate; }}// Use the React Context to pass dispatches
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;
Copy the code

Let’s examine the code changes above one by one:

  1. The initial state of the entire application is definedinitialStateThis is the backuseReducerWhat the hook needs
  2. Then we defined the Reducer function, which mainly responds to three actions:SET_KEYSET_COUNTRYSET_LASTDAYSIs used to modify data indicator, country and past days respectively
  3. Defines theAppDispatchThis Context is used to pass to the child componentsdispatch
  4. calluseReducerHook to get the statestateAnd the distribution functiondispatch
  5. And finally for renderingAppDispatch.ProviderWrap the entire application, pass indispatchTo make the child components available

Modify the state with Dispatch in the child component

Now that all the state of the child component has been extracted into the root component, the only thing the child has to do is change the central state through Dispatch in response to user events. The idea is simple:

  • Through the firstuseContextAccess to theAppComponent is passed downdispatch
  • calldispatchAnd initiate corresponding actions.

OK, let’s get started. Open the SRC/components/CountriesChart js, modify the code is 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;
Copy the code

In the same train of thought, let’s modify SRC/components/HistoryChartGroup js components:

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

The last kilometer, 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;
Copy the code

Once the refactoring is complete and the project is up and running, you should find exactly what you did in the previous step.

prompt

If you are familiar with Redux, there is a small regret in our refactoring: the child component can only get the state in the root component App by passing Props. An alternative is to insert state into the Context as well, but if this is the case, I recommend using Redux directly.

Is Redux still useful? Control vs. Context

Hear some voices saying Redux is not needed anymore thanks to React Hooks. So is Redux still useful?

Before I answer that question, allow me to throw my hat in the ring. React Hooks are terrifically powerful, especially with the excellent third-party custom Hooks library that allows almost every component to handle complex business logic. In contrast, Redux’s core idea is to centralize the state and the operation to modify the state.

Do you see that this actually corresponds to two management ideas Context and Control?

The manager needs Context, not Control. — Zhang Yiming, founder and CEO of Bytedance

Control is the centralization of power so that employees can methodically perform tasks according to the DECISIONS of the CEO, just as the Global Store in Redux is the “Single Source of Truth”. All state and data flow updates must go through the Store; Context gives departments and levels enough decision-making power because they have more Context and better expertise, just as React components that respond to specific logic have more Context and can perform tasks “on their own” with Hooks instead of relying on the global Store.

At this point, I think you already have your answer in mind. If you want to share, be sure to leave a comment in the comments section

The resources

  • Sarah Drasner: Understanding the Almighty Reducer
  • Kingsley Silas: Getting to Know the useReducer React Hook
  • Kpax Qin: Pain points, analysis and improvement of Redux state management
  • Try to use useReducer instead of useState.
  • Zhang Yiming: CEOS should avoid “rational conceit “, which is a mistake made by Gates and Jobs

Want to learn more exciting practical skills tutorial? Come and visit the Tooquine community.