简体   繁体   中英

Using fetch in a custom React hook - data is fetching twice

My useFetch custom hook:

import React, { useState, useEffect } from 'react';

const useFetch = (url, method = 'get') => {
  const [response, setResponse] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    (async () => {
      try {
        setLoading(true);
        const res = await fetch(url, {
          signal: controller.signal,
          method: method.toUpperCase()
        });
        const json = await res.json();
        setResponse(json);
        setLoading(false);
        console.log('data fetched!!!', url);
      } catch (error) {
        if (error.name === 'AbortError') {
          setError('Aborted!');
        } else {
          setError(error);
        }
        setLoading(false);
      }
    })();

    return () => {
      controller.abort();
    };
  }, [url]);

  return { response, loading, error };
};

export default useFetch;

Using this hook inside the following component hierarchy: Catalog > Pagination.

Contents of Catalog component, where do i use a useFetch hook:

const Catalog = () => {
  const [limit] = useState(12);
  const [offset, setOffset] = useState(0);

  const { response, loading, error } = useFetch(
    `http://localhost:3000/products?${new URLSearchParams({
      limit,
      offset
    })}`
  );

  // onPageChange fired from Pagination component, whenever i click on any of the page numbers.
  const onPageChange = index => {
    setOffset(index);
  };

  return (
    <Pagination
      items={response.data}
      count={response.count}
      pageSize={limit}
      onPageChange={onPageChange} 
    />
  );
}

Then, a Pagination component:

const Pagination = ({ items, count, pageSize, onPageChange }) => {
    useEffect(() => {
    if (items && items.length) {
      setPage(1)();
    }
  }, []);

  const setPage = page => e => {
    if (e) e.preventDefault();
    let innerPager = pager;

    if (page < 1 || page > pager.totalPages) {
      return null;
    }

    innerPager = getPager(count, page, pageSize);
    setPager(innerPager);
    onPageChange(innerPager.startIndex); // fires a function from the Catalog component
  };

const getPager = (totalItems, currentPage, pageSize) => {
    currentPage = currentPage || 1;
    pageSize = pageSize || 10;
    const totalPages = Math.ceil(totalItems / pageSize);

    let startPage, endPage;
    if (totalPages <= 10) {
      startPage = 1;
      endPage = totalPages;
    } else {
      if (currentPage <= 6) {
        startPage = 1;
        endPage = totalPages;
      } else if (currentPage + 4 >= totalPages) {
        startPage = totalPages - 9;
        endPage = totalPages;
      } else {
        startPage = currentPage - 5;
        endPage = currentPage + 4;
      }
    }

    let startIndex = (currentPage - 1) * pageSize;
    let endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1);

    const pages = [...Array(endPage + 1 - startPage).keys()].map(
      i => startPage + i
    );

    return {
      totalItems,
      currentPage,
      pageSize,
      totalPages,
      startPage,
      endPage,
      startIndex,
      endIndex,
      pages
    };
  };

  return (
    <a className="page-link" href="#" onClick={e => setPage(page)(e)}>{page}</a>
  ); // shortened markup, i'm mapping paginator buttons from a getPager() function, each button has onClick event listener, which fires a setPage()()
}

A problem i currently experience now, is when i click on a pagination button, eg: 1, 2, 3, my useFetch hook fires a first time with a correct offset value, for example: if i click on a 2nd page, offset would be equal to 12, if i click on a 3rd page, offset would be 24 and so on.

But in my case a useFetch hook is fired twice, first time with correct offset data, then a 2nd time, with initial offset data = 0. I'm only watching for the url changes inside useFetch hook, so my offset is changed just once, when i press on a pagination button.

Based on the conversation I had with OP, the main issue was the useEffect inside of Pagination component that would get triggered every time the component mounted. Even though the useEffect used an empty dependency array, which would prevent calling the useEffect hook once again when new props got passed, the Pagination component was getting unmounted because of the logic inside Catalog .

// it more or less looked like this

const Catalog = () => {
  const {response, loading, error} = useFetch(...)

  const onPageChange = (index) => {...}

  if (loading) { return `Loading` }

  return <Pagination {...props} />
}

When loading was set back to true after calling onPageChange from the Pagination component, the useEffect hook inside of that same component would get called with a constant, setPage(1)() and reset the newly fetch items to the default offset , which is 1 in this case.

The if (loading) statement would unmount the Pagination component when it evaluated to true , causing the useEffect to run again inside Pagination because it was mounting for the "first time."

const Pagination = (props) => {
  useEffect(() => {
    if (...) setPage(1)()

    // empty array will prevent running this hook again
    // if props change and component re-renders, 
    // but DOES NOT get unmounted and mounted again
  }, [])
}

The OP needs to figure out the current page number inside of the Pagination component, hence the useEffect that called setPage(1)() . My suggestion was to move that business logic to the useFetch hook instead and figure out the current page number based on the response from the API.

I tried to come up with a solution and created a new hook that was more explicit, called useFetchPosts , that utilizes the useFetch hook, but returns the page , and pageCount along with other relevant data from the API.

function useFetchPosts({ limit = 12, offset = 0 }) {
  const response = useFetch(
    `https://example.com/posts?${new URLSearchParams({ limit, offset })}`
  );
  const [payload, setPayload] = React.useState(null);

  React.useEffect(() => {
    if (!response.data) return;

    const { data, count } = response.data;

    const lastItem = [...data].pop();
    const page = Math.floor(lastItem.id / limit);
    const pageCount = Math.ceil(count / limit);

    setPayload({
      data,
      count,
      page,
      pageCount
    });
  }, [response, limit, offset]);

  return {
    response: payload,
    loading: response.isFetching,
    error: response.error
  };
}

So now, instead of calculating what the current page is inside of Pagination , we can get rid of the useEffect hook that was causing issues and just simply pass that information to the Pagination component from Catalog .

function Catalog() {
  const [limit] = React.useState(12);
  const [offset, setOffset] = React.useState(1);

  const { response, loading, error } = useFetchPosts({
    limit,
    offset
  });

  // ...

  return (
    <Pagination
      items={response.data}
      count={response.count}
      pageSize={limit}
      pageCount={response.pageCount}
      page={response.page}
      onPageChange={onPageChange}
    />
  );
}

This can be further optimized and perhaps done a little better, but here's an example that does what it's supposed to when different pages are clicked:

Hope this helps.

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