How to trigger a hook once a state has changed in a functional component?

Viewed 188

I'm writing a custom pagination hook that'll fetch an API and will return some data to me like React Query. Fairly simple. What I'm trying to do is to trigger that hook when a state(in this case cursor) updates.

Here's the hook itself:

import axios from "../api/axios";
import { useEffect, useRef, useState } from "react";

interface FetchInfiniteResourceProps {
  // The URL of the API endpoint
  url: string;
  // Cursor of the page
  cursor: number;
  // Parameter with which the backend accepts the cursor number
  queryParam: string;
  // The property on the response which will indicate if the next page exists
  nextPageParam: string;

  // Other config
  config: {
    enabled: boolean;
  };
}

// Prop types for useFetchInfiniteResource
interface StateProps {
  data: any[];
  isLoading: boolean;
  hasNextPage: boolean;
  isFetchingNextPage: boolean;
}

/**
 * This function will be used for fetching
 * an infinite resource from the back-end
 */
export const useFetchInfiniteResource = ({
  url,
  cursor,
  queryParam,
  nextPageParam,
  config: { enabled },
}: FetchInfiniteResourceProps) => {
  // Creating a new state
  const [state, setState] = useState<Partial<StateProps>>({
    data: [],
    isLoading: true,
    hasNextPage: false,
    isFetchingNextPage: false,
  });

  // Method for setting the data
  const setData = (newState: []) => setState({ data: newState });

  // The function for sending the request
  const request = async () => {
    // If it's not the first time that this request has triggered
    if (!state.isLoading) setState({ ...state, isFetchingNextPage: true });

    // Sending the request
    const { data: response } = await axios.get(url, {
      params: {
        // Query paramter
        [queryParam]: cursor,
      },
    });

    // Updating the state
    setState({
      // If it's the first time that this request is invoked
      isLoading: state?.isLoading && false,
      // Merging the data
      data: state?.data?.concat(response.data),
      // Checking if the next page exists
      hasNextPage: response[nextPageParam] !== null,
      // If it's not the first time that the request is invoked then set it to false
      isFetchingNextPage: !state?.isLoading && false,
    });
  };

  const requestRef = useRef(async () => await request());

  useEffect(() => {
    if (enabled) requestRef.current();
  }, [enabled]);

  // Returning all the items from the state and the function to update the data manually
  return { ...state, setData };
};

And here's the component:

const SomeComponent = () => {
    const [cursor, setCursor] = useState(0);
    const {...} = useFetchInfiniteResource({ cursor, ... }); // Problem here

    someListener(() => {
       setCursor(cursor + 1); // Problem here
    });

    return ...
}

I think I'm misusing the useEffect or the useRef hook, but so far I have tried also using the useCallback hook.

Thanks for your help

1 Answers

Try wrapping the request function in a useEffect hook with just the cursor in the dependency array. ESLint won't like it, but it should work.

Stripped down example:

const useFetchInfiniteResource = ({
  cursor,
  ...
}: FetchInfiniteResourceProps) => {
  const [state, setState] = useState<Partial<StateProps>>({ ... });
 
  const setData = (newState: []) => setState({ data: newState });
 
  useEffect(() => {
    const request = async () => {
      if (!state.isLoading) setState({ ...state, isFetchingNextPage: true });
      const { data: response } = await axios.get(url, {
        params: {
          [queryParam]: cursor,
        },
      });
      setState({ ... });
    };
 
    if (enabled) request();
  }, [cursor]);

  return { ... };
};
Related