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 💪

In the second tutorial, we’ll walk you through refactoring the previous component code using custom hooks to make it cleaner and logically reusable. After the refactoring was complete, we were stuck in an endless loop of components “getting data and re-rendering” when useCallback came in and saved our application like a magic pin…

Welcome to the GitHub repository and Gitee repository of this project.

Custom Hook: customized

In the last tutorial, we went into use estate and useEffect with animations, basically clarifying the implementation mechanism behind React Hooks — linked lists, as well as implementing a global data overview of COVID-19 data visualization applications and histograms of data from multiple countries.

If you want to read and practice with this tutorial directly, you can download the source code for this tutorial:

git clone -b second-part https://github.com/tuture-dev/covid-19-with-hooks.git
# or clone Gitee's warehouse
git clone -b second-part https://gitee.com/tuture/covid-19-with-hooks.git
Copy the code

Custom Hooks are the most interesting feature, or feature, of React Hooks. In short, it allows you to share certain logic between different functional components in a highly flexible way. Let’s start with a very simple example.

A simple custom Hook

Let’s start with a Hook called useBodyScrollPosition that gets the vertical scroll position of the current browser:

function useBodyScrollPosition() {
  const [scrollPosition, setScrollPosition] = useState(null);

  useEffect((a)= > {
    const handleScroll = (a)= > setScrollPosition(window.scrollY);
    document.addEventListener('scroll', handleScroll);
    return (a)= >
      document.removeEventListener('scroll', handleScroll); } []);return scrollPosition;
}
Copy the code

Through observation, we can find that custom Hook has the following characteristics:

  • On the surface: a naming format foruseXXXFunction, but not the React function component
  • Essentially: Internally by using the React built-in hooks (e.guseState å’Œ useEffect) to implement some general logic

If you think outside the box, you can think of many places to do custom hooks: DOM side effects modification/listening, animation, requests, form manipulation, data storage, and more.

prompt

Two powerful React Hooks libraries are recommended: React Use and Umi Hooks. Both implement a number of production-level custom hooks that are well worth learning.

I think that’s the biggest appeal of React Hooks — with a few built-in Hooks, you can “make” any Hook you really need by any combination of conventions, or by invoking Hooks written by someone else, to deal with complex business scenarios with ease. It’s as if the world is made up of more than 100 elements.

Take a look at the principles behind custom hooks

It’s animation time again. Let’s look at what happens when the component is first rendered:

We called the useCustomHook hook in the App component. As you can see, even if we switch to custom hooks, the generation of the Hook list remains the same. Let’s look at the case where rendering is important:

Similarly, even if the execution of the code goes into a custom Hook, we can still read the corresponding data from the Hook list, and this “pairing” process always succeeds.

Let’s revisit Rules of Hook. It states that React Hook can only be used in two places:

  1. React function component
  2. Customize the Hook

The first point is already clear, and the second point is also clear to you through the two animations just now: in essence, custom Hook only encapsulates the process of calling built-in Hook into one reusable function, and does not affect the generation and reading of Hook linked list.

Practical link

Let’s continue the development of COVID-19 data applications. Next, we intend to present historical data, including confirmed cases, deaths and cures.

Let’s start by implementing a custom Hook called useCoronaAPI for sharing logic to get data from the NovelCOVID 19 API. Create a SRC/hooks/useCoronaAPI js, fill in the code is as follows:

import { useState, useEffect } from "react";

const BASE_URL = "https://corona.lmao.ninja/v2";

export function useCoronaAPI(path, { initialData = null, converter = (data) = >data.refetchInterval = null}){
  const [data, setData] = useState(initialData);

  useEffect((a)= > {
    const fetchData = async() = > {const response = await fetch(`${BASE_URL}${path}`);
      const data = await response.json();
      setData(converter(data));
    };
    fetchData();

    if (refetchInterval) {
      const intervalId = setInterval(fetchData, refetchInterval);
      return (a)= > clearInterval(intervalId);
    }
  }, [converter, path, refetchInterval]);

  return data;
}
Copy the code

As you can see, the useCoronaAPI defined contains two parameters. The first parameter is path, which is the API path. The second is the configuration parameters, including the following parameters:

  • initialData: Default data that is initially empty
  • converter: Conversion function to raw data (default is an identity function)
  • refetchInterval: Interval (in milliseconds) between retrieving data

Also, note that useEffect is passed in the DEPS array, which contains three elements (all used in the Effect function) : Converter, Path, and refetchInterval, all from parameters passed in by the useCoronaAPI.

prompt

In the last article, we briefly mentioned not to lie about useEffect dependencies, so here is a good example: we add all external data (including the function) used by the Effect function to the dependency array. Of course, BASE_URL is a module-level constant, so you don’t need it as a dependency. But there’s a hole here, heh heh…

Then use the useCoronaAPI hook you just created in the root component SRC/app.js as follows:

import React, { useState } from "react";

// ...
import { useCoronaAPI } from "./hooks/useCoronaAPI";

function App() {
  const globalStats = useCoronaAPI("/all", {
    initialData: {},
    refetchInterval: 5000});const [key, setKey] = useState("cases");
  const countries = useCoronaAPI(`/countries? sort=${key}`, {
    initialData: [].converter: (data) = > data.slice(0.10)});return (
    // ...
  );
}

export default App;
Copy the code

The whole App component is a lot clearer, isn’t it?

But when we run the app with great anticipation, we find that the whole app gets stuck in a cycle of “infinite requests.” Open the Network TAB of Chrome developer Tools and you’ll see the number of web requests skyrockets all the time…

So scared we shut it down. Calm down, can not help thinking: why on earth?

dangerous

NovelCOVID 19 API is a public welfare data resource, so we should close the page as soon as possible to avoid too much request pressure on the server of the other party.

UseCallback: To calm the sea

If you read the last passage, you might have found a clue:

Object. Is is used to compare dependent arrays to determine whether elements have changed. Therefore, when an element in DEPS is of non-original type (such as function, Object, etc.), every rendering will change, thus triggering Effect every time and losing the meaning of DEPS itself.

OK, if you don’t remember that, let’s talk about an issue that often comes to me when I’m learning React Hooks: Effect infinite loops.

About Effect infinite loop

Take a look at this “never Stop” counter:

function EndlessCounter() {
  const [count, setCount] = useState(0);

  useEffect((a)= > {
    setTimeout((a)= > setCount(count + 1), 1000);
  });

  return (
    <div className="App">
      <h1>{count}</h1>
    </div>
  );
}
Copy the code

If you run this code, you’ll see that the numbers are always growing. Let’s use an animation to illustrate how this “infinite loop” works:

Our component is stuck in an infinite loop: Render => trigger Effect => modify state => trigger rerender.

Presumably you’ve already found the culprit behind the useEffect falling into an infinite loop — not providing the correct deps! This results in the Effect function being executed after each rendering. In fact, in the previous useCoronaAPI, there was also a problem with the incoming DEPS, which caused the Effect function to be executed after each rendering to fetch data, falling into an infinite loop. So which dependency is going wrong?

That’s right, the Converter function! We know that in JavaScript there is a huge difference between primitive and non-primitive types in determining whether the values are the same:

// Primitive type
true= = =true // true
1= = =1 // true
'a'= = ='a' // true

// Non-primitive type= = = {} {}// false[] [] = = =// false() = > {} = = =(a)= > {} // false
Copy the code

Also, each time the Converter function is passed in the same form, it is still a different function (with unequal references), resulting in the Effect function being executed each time.

About Memoization

Memoization, commonly called Memoization caching (or “memory”), sounds like a fancy computer term, but the idea behind it is simple: If we have a large amount of calculation of pure functions (given the same input, will get the same output), then we when encountered for the first time a particular input, the output of it “” (cache), then the next time you run into the same output, only need to take out from the cache returned directly, eliminating the calculation process.

In fact, in addition to saving unnecessary computation and thus improving program performance, Memoization is also used to ensure that references to returned values are equal.

Let’s familiarize ourselves with Memoization by taking a simple square root function. The first is an uncached version:

function sqrt(arg) {
  return { result: Math.sqrt(arg) };
}
Copy the code

You may have noticed that we specifically returned an object to record the results, which we will compare to the Memoized version later. Then there is the cached version:

function memoizedSqrt(arg) {
  // If the cache does not exist, an empty object is initialized
  if(! memoizedSqrt.cache) { memoizedSqrt.cache = {}; }// If there is no cache hit, the result is calculated first, then cached, and then returned
  if(! memoizedSqrt.cache[arg]) {return memoizedSqrt.cache[arg] = { result: Math.sqrt(arg) };
  }

  // Returns the result directly in the cache without calculation
  return memoizedSqrt.cache[arg];
}
Copy the code

We then try to call both functions and see some obvious differences:

sqrt(9)                      // { result: 3 }
sqrt(9) === sqrt(9)          // false
Object.is(sqrt(9), sqrt(9))  // false

memoizedSqrt(9)                              // { result: 3 }
memoizedSqrt(9) === memoizedSqrt(9)          // true
Object.is(memoizedSqrt(9), memoizedSqrt(9))  // true
Copy the code

While a normal SQRT returns a different reference to the result each time (or a brand new object), memoizedSqrt can return exactly the same object. In React, Memoization ensures that Prop or state references are equal across multiple renders, thus avoiding unnecessary rerenders or side effects.

Let’s summarize two usage scenarios for Memoization:

  • Save time by caching the results
  • Ensure that references to return values of the same input are equal

Use method and principle analysis

React introduces an important Hook, useCallback, to solve the Referential Equality problem of functions in multiple renderings. The official documents describe the usage methods as follows:

const memoizedCallback = useCallback(callback, deps);
Copy the code

The first parameter callback is the function to remember. The second parameter is the familiar DEps parameter, which is also a dependent array (sometimes called inputs). In the context of Memoization, this DEPS acts like a Key in the cache, and if the Key does not change, then the function in the cache is returned directly, making sure to refer to the same function.

In most cases, we pass the empty array [] as the deps argument, so that useCallback always returns the same function and is never updated.

prompt

You may have noticed when you first learned about useEffect that we don’t need the second Setter returned by useState as a dependency on Effect. In fact, there is Memoization of Setter functions in React, so the Setter functions rendered are exactly the same every time. It doesn’t matter whether dePS is added or not.

As usual, we’ll use an animation to see how useCallback works (when dePS is an empty array), starting with the first render:

As before, the useCallback call is appened to the Hook list, but we emphasize the memory location that f1 points to, which makes it clear that f1 always points to the same function. OnClick is then returned to point to f1 stored in the Hook.

Let’s look at the case where rendering is important:

When rerendering, calling useCallback again returns the f1 function, which points to the same block of memory, making the onClick function truly referential equal to the last rendering.

The relationship between useCallback and useMemo

We know that useCallback has a good gay friend named useMemo. Remember the two scenes we summarized for Memoization earlier? UseCallback is mainly designed to solve the “reference equality” problem of functions, while useMemo is a “generalist” capable of both reference equality and saving calculation tasks.

In fact, useMemo functions as a superset of useCallback. Whereas useCallback can only cache functions, useMemo can cache any type of value (including functions, of course). UseMemo can be used as follows:

const memoizedValue = useMemo((a)= > computeExpensiveValue(a, b), [a, b]);
Copy the code

The first argument is a function whose return value (that is, the result of computeExpensiveValue above) will be returned to memoizedValue. So the use of the following two hooks is completely equivalent:

useCallback(fn, deps);
useMemo((a)= > fn, deps);
Copy the code

Given the relatively few computationally intensive tasks encountered in front-end development, and the performance of the browser engine is good enough, this series of articles will not go into the use of useMemo. Besides, if you already know how to use useCallback, you should already know how to use useMemo.

Practical link

Now that we’re familiar with the useCallback, we’re fixing the useCoronaAPI hooks. Modify the SRC /hooks/useCoronaAPI as follows:

import { useState, useEffect, useCallback } from "react";

// ...

export function useCoronaAPI(
  //.) {
  const [data, setData] = useState(initialData);
  const convertData = useCallback(converter, []);

  useEffect((a)= > {
    const fetchData = async() = > {// ...
      setData(convertData(data));
    };
    fetchData();

    // ...
  }, [convertData, path, refetchInterval]);

  return data;
}
Copy the code

As you can see, we wrapped the Converter function in useCallback, named the mnemonized function convertData, and passed the deps argument as an empty array [] to make sure it was the same every time. Then change all converter functions in useEffect to convertData accordingly.

Finally, I started the project again, and everything returned to normal. This custom Hook reconstruction was successfully completed! In the next tutorial, we will begin to take the COVID-19 data Visualization project further, graphing historical data (including diagnoses, deaths, and cures). As data states become more complex, what can we do about them? Stay tuned.

Use useReducer + useContext to implement a simple Redux!

The resources

  • React
  • Dt-fe: How to build wheels from React Hooks
  • WellPaidGeed: How to write custom hooks in React
  • Netlify Blog: Deep Dive: How do React hooks really work?
  • Andrew Myint: How to Fix the Infinite Loop Inside “useEffect” (React Hooks)
  • Kent C. Dodds: When to useMemo and useCallback
  • Sandro Dolidze: React Hooks: Memoization
  • Chidume Nnamdi: Understanding Memoization in JavaScript to Improve Performance

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