reactjsreact-hooksfetch-api

Invalid hook call when making a reusable fetch function


I'm trying to make reusable GET and POST requests, but I'm getting error Invalid hook call when setting the data state in the reusable fetch request component. I've just learnt that we can only use hooks in React components, so how can I make that functionality reusable?

import axios from "axios";
import { useContext, useEffect, useState } from "react";
import AuthContext from "../../context/Auth-context/AuthContext";
import { useNavigate } from "react-router-dom";



export const POST = (url, body, credentials) => {
  const [data, setData] = useState([]);
  const [isLoaded, setIsLoaded] = useState(false);
  const [error, setError] = useState(null);
  const { dispatch } = useContext(AuthContext);
  const navigate = useNavigate();


  useEffect(() => {
    const fetchData = () => {
      axios
        .post(url, body, { withCredentials: credentials })
        .then((response) => {
          setIsLoaded(true);
          setData(response.data);
        })
        .catch((error) => {
          if (error.status === 401) {
            dispatch({ type: "LOGOUT" });
            navigate("/login");
            return;
          }
          setError(error);
        });
    };
    fetchData();
  }, [url, credentials, dispatch, navigate, body]);

  return { error, isLoaded, data };
};
  const approve = async (id) => {
    const { data, error, isLoaded } = POST(
      "http://localhost:8000/api/new-users",
      id,
      true
    );
    if (error) {
      return (
          <Error/>
      );
    }
    if (!isLoaded) {
      return (
          <Loader/>
      );
    }

    console.log(data);

  };


Solution

  • The code is breaking React's Rules of Hooks. React hooks, i.e. useState, useNavigate, etc, can only be called in React functions and custom React hooks, and POST is neither of these. The trivial solution would be to simply rename POST to be a valid React hook, e.g. using the "use-" prefix on the identifier. The issue then is that you would be calling usePost in a callback which again breaks React's Rules of Hooks.

    I suggest a refactor to return a "trigger" function that handles the fetch (instead of useEffect) which can be called in a callback.

    Example:

    export const usePost = (url, credentials) => {
      const [data, setData] = useState([]);
      const [isLoaded, setIsLoaded] = useState(false);
      const [error, setError] = useState(null);
      const { dispatch } = useContext(AuthContext);
      const navigate = useNavigate();
    
      const trigger = useCallback((body) => {
        return axios
          .post(url, body, { withCredentials: credentials })
          .then((response) => {
            setData(response.data);
          })
          .catch((error) => {
            if (error.status === 401) {
              dispatch({ type: "LOGOUT" });
              navigate("/login");
              return;
            }
            setError(error);
          })
          .finally(() => {
            setIsLoaded(true);
          });
      }, [url, credentials, dispatch, navigate]);
    
      return { error, isLoaded, data, trigger };
    };
    

    Usage:

    const { data, error, isLoaded, trigger } = usePost(
      "http://localhost:8000/api/new-users",
      true
    );
    
    ...
    
    const approve = async (id) => {
      trigger(id);
    }
    
    ...
    
    if (error) {
      return <Error />;
    }
    
    if (!isLoaded) {
      return <Loader />;
    }
    
    ...