reactjsreact-hooksinfinite-scrollreact-custom-hooks

custom hook doesn't update when parent state changes


I'm new and may have said the terms wrong I tried to write an infinite scroll from the code below github

By running the app, a few basic items are taken from the API, and when I scroll down, start changes, but a new request to get data is not sent.

my custom hook

import { useEffect, useState } from "react";
import { getData } from "../apis/coinmarketcapApi";

export default function useData({ start = 0 }) {
  const [result, setResult] = useState([]);
  const [error, setError] = useState({});
  const [isError, setIsError] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [hasNextPage, setHasNextPage] = useState(false);
  
  console.log('start-useData :>> ', start);

  useEffect(() => {
    setIsLoading(true);
    setIsError(false);
    setError({});

    const controller = new AbortController();
    const { signal } = controller;

    getData(start, { signal })
      .then((response) => {
        setResult((prev) => [...prev, ...response.data]);
        setHasNextPage(Boolean(response.data.length));
        setIsLoading(false);
      })
      .catch((e) => {
        setIsLoading(false);
        if (signal.aborted) return;
        setIsError(true);
        setError({ "axios error:": e });
      });

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

  return { result, error, isError, isLoading, hasNextPage };
}

parent component

import React, { useState, useCallback, useRef } from "react";
import useData from "./hooks/useData";
import Post from "./Post";

export default function CryptoList() {
  const [start, setStart] = useState(0);
  const { result, error, isError, isLoading, hasNextPage } = useData(start);
  console.log('start :>> ', start);

  const intersectionObserver = useRef();
  const lastDataRef = useCallback(
    (data) => {
      if (isLoading) return;

      if (intersectionObserver.current)
        intersectionObserver.current.disconnect();

      intersectionObserver.current = new IntersectionObserver((entry) => {
        if (entry[0].isIntersecting && hasNextPage)
          setStart((prev) => prev + 10);
      });

      if (data) intersectionObserver.current.observe(data);
    },
    [hasNextPage, isLoading]
  );

  if (isError) return <h1>Error: {error.message}</h1>;

  const content = result.map((data, index) => {
    if (result.length === index + 1)
      return <Post key={data.id} data={data} ref={lastDataRef} />;

    return <Post key={data.id} data={data} />;
  });

  return (
    <>
      <h1>List of Crypto</h1>
      {content}
      {isLoading && <h2>Loading ...</h2>}
    </>
  );
}

console

enter image description here

i want to with change of state, custom hook re-render


Solution

  • The useData hook will be called only once when the component will be created. The hook function does not run on every rerender. To achieve your desired functionality, create another state inside your hook, and expose the setter function. By doing this, you can update the start state inside the hook and thus trigger useEffect.

    import { useEffect, useState } from "react";
    import { getData } from "../apis/coinmarketcapApi";
    
    export default function useData() {
      const [result, setResult] = useState([]);
      const [error, setError] = useState({});
      const [isError, setIsError] = useState(false);
      const [isLoading, setIsLoading] = useState(false);
      const [hasNextPage, setHasNextPage] = useState(false);
      const [start, setStart] = useState(0);
      console.log('start-useData :>> ', start);
    
      useEffect(() => {
        setIsLoading(true);
        setIsError(false);
        setError({});
    
        const controller = new AbortController();
        const { signal } = controller;
    
        getData(start, { signal })
          .then((response) => {
            setResult((prev) => [...prev, ...response.data]);
            setHasNextPage(Boolean(response.data.length));
            setIsLoading(false);
          })
          .catch((e) => {
            setIsLoading(false);
            if (signal.aborted) return;
            setIsError(true);
            setError({ "axios error:": e });
          });
    
        return () => controller.abort();
      }, [start]);
    
      return { result, error, isError, isLoading, hasNextPage, setStart };
    }
    

    Now you can use setStart inside a useEffect in the parent component