简体   繁体   中英

React: Custom Fetch Hook with or without useCallback?

I am currently teaching myself React, and have stumbled upon the following code snipped for a custom hook that uses the fetch() method.

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

function useFetch(query, page) {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [list, setList] = useState([]);

  const sendQuery = useCallback(async () => {
    try {
      await setLoading(true);
      await setError(false);
      const res = await axios.get(url);
      await setList((prev) => [...prev, ...res.data];
      setLoading(false);
    } catch (err) {
      setError(err);
    }
  }, [query, page]);

  useEffect(() => {
    sendQuery(query);
  }, [query, sendQuery, page]);

  return { loading, error, list };
}

export default useFetch;

I can't quite understand the use of useCallback() in this context. My fetch() hooks so far only imported useState() and useEffect() and then called useEffect() whenever one of the parameters changed. Can somebody explain to me what exactly useCallback() does and when I should use it to fetch data from an API?

According to offical doc: https://reactjs.org/docs/hooks-reference.html#usecallback .
UseCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders

So, in your case, It's not using the memoized version for children, but it uses to the useEffect so the useEffect will triggered when query or page changes, to be honest, it doesn't have to be this way, for me this is a bit over-engineered, you could also do it something like this:

useEffect(async () => {
  try {
    await setLoading(true);
    await setError(false);
    const re
s = await axios.get(url);
    await setList((prev) => [...prev, ...res.data];
    setLoading(false);
  } catch (err) {
    setErr
or(err);
  }
}, [query, page])

so it will be simplified only useEffect, I don't see the point of useCallback here.

I think I have found the problem: If you have a component, and a function that handles user input by calling the API (using the hook) with the current value whenever it changes, this inner function is re-created whenever the component re-renders, which happens every time anything inside it changes. This means that when anything else inside the component changes, the API is called again.

This problem can be solved by wrapping the API call into the useCallback() hook and passing it the input value as a dependency, so that the API is only called when the input changes, and the last response is memoized between re-renders of the component.

I have found a video that is a great explanation .

useCallback memoizes the function definition, so that each time a component re-renders it won't create a new definition for that function unless the dependency array fed to useCallback changes.

Here, the function with useCallback is included in a dependency array for the useEffect . Without a memoization, each time this hook is called, it will create a new function definition for searchQuery and will render again, resulting in an infinite loop.

To visualize this I have created a codesandbox example , open up console and see that the component that doesn't use the useCallback renders infinitely.

The Code:

-- Query.jsx

import useFetchWithCallback from "./useFetch";
import useFetchWithoutCallback from "./useFetchWithoutCallback";

const url = "https://jsonplaceholder.typicode.com/todos/1";

export const QueryWithCallback = () => {
  const { data } = useFetchWithCallback({
    url
  });
  console.log("Query with callback rerender");

  return <h1>Query with callback: {data?.[0]?.id}</h1>;
};

export const QueryWithoutCallback = () => {
  const { data } = useFetchWithoutCallback({
    url
  });
  console.log("Query without callback rerender");

  return <h1>Query without callback: {data?.[0]?.id}</h1>;
};

-- useFetch.js

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

function useFetch({ query, page, url }) {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [list, setList] = useState([]);

  const sendQuery = useCallback(async () => {
    try {
      setLoading(true);
      setError(false);
      const res = await axios.get(url);
      setList(res.data);
      setLoading(false);
    } catch (err) {
      setError(err);
    }
  }, [url]);

  useEffect(() => {
    sendQuery();
  }, [sendQuery]);

  return { loading, error, data: list };
}

export default useFetch;

--useFetchWithoutCallback.js

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

function useFetch({ query, page, url }) {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [list, setList] = useState([]);

  const sendQuery = async() => {
    try {
      setLoading(true);
      setError(false);
      const res = await axios.get(url);
      setList(res.data);
      setLoading(false);
    } catch (err) {
      setError(err);
    }
  }

  useEffect(() => {
    sendQuery();
  }, [sendQuery]);

  return { loading, error, data: list };
}

export default useFetch;

Note: There are other use cases for useCallback . Specifically, not re-rendering child component when it is used with React.memo(...)

You can use it without useCallback. Dont use useEffect asynchronously, The issue here is that the first argument of useEffect is supposed to be a function that returns either nothing (undefined) or a function (to clean up side effects). But an async function returns a Promise, which can't be called as a function. It's simply not what the useEffect hook expects for its first argument.

https://devtrium.com/posts/async-functions-useeffect

you can use it like

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

function useFetch(query, page) {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [list, setList] = useState([]);


  useEffect(() => {
   const sendQuery = async () => {
    try {
      await setLoading(true);
      await setError(false);
      const res = await axios.get(url);
      await setList([...list, ...res.data]);
      setLoading(false);
    } catch (err) {
      setError(err);
    }
   }
    sendQuery(query);
  }, []); 

  return { loading, error, list };
}

export default useFetch;

The React useCallback Hook returns a memoized callback function.

Think of memoization as caching a value so that it does not need to be recalculated.

This allows us to isolate resource intensive functions so that they will not automatically run on every render.

The useCallback Hook only runs when one of its dependencies update.

This can improve performance.

The useCallback and useMemo Hooks are similar. The main difference is that useMemo returns a memoized value and useCallback returns a memoized function.

https://www.w3schools.com/react/react_usecallback.asp

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM