0

I have a project where I created a custom hook "useApi" that manage calls to my backend API to avoid code repetition everytime I need to call the API.

This useApi hook return an array with a loading state as first parameter, and the function to call the API as second parameter.

The loading state is set to true by default, and is set to false when fetch is done.

My problem comes here, in some scenarios I need to call the API on events (clicks and forms submit). In that case I would prefere the loading state to false by default, then set it to true before the fetch, and back to false when fetch is done. In the code of my custom hook below, I commented the lines setLoading(true) before the fetch otherwise it causes an infinite loop.

I think I kind of understand why this infinite loop because of the loading state update. But I can't find a solution on how to achieve this "dynamic" loading state

import { useState } from "react";

export default function useApi({method, url}) {

    const [loading, setLoading] = useState(true);

    const abortController = new AbortController();
    const fetchBaseUrl = import.meta.env.VITE_APP_URL + '/api/' + url;

    const methods = {
        get : function(data = {}) {
            // setLoading(true);
            const params = new URLSearchParams(data);
            const queryString = params.toString();
            const fetchUrl = fetchBaseUrl + (queryString ? "?"+queryString : "");
            
            return new Promise((resolve, reject) => {
                fetch(fetchUrl, {
                    signal : abortController.signal,
                    headers: {
                        "Content-Type": "application/json",
                        "Accept": "application/json",
                    },
                })
                .then(response => response.json())
                .then(data => {
                    setLoading(false);
                    if (!data) {
                        return reject(data);
                    }
                    resolve(data);
                })
                .catch(error => {
                    setLoading(false);
                    if (!abortController.signal.aborted) {
                        reject(error);
                    }
                });
            });
        },
        post :  function(data = {}) {
            // setLoading(true);
            const bodyData = {
                signal : abortController.signal,
                ...data
            };

            return new Promise((resolve, reject) => {
                fetch(fetchBaseUrl, {
                    method: "post",
                    headers: {
                        "Content-Type": "application/json",
                        "Accept": "application/json",
                    },
                    body: JSON.stringify(bodyData)
                })
                .then(response => response.json())
                .then(data => {
                    setLoading(false);
                    if (!data) {
                        return reject(data);
                    }
                    resolve(data);
                })
                .catch(error => {
                    setLoading(false);
                    if (!abortController.signal.aborted) {
                        reject(error);
                    }
                });
            });
        }
    }

    if ( !(method in methods) ) {
        throw new Error("Incorrect useApi() first parameter 'method'")
    }

    return [loading, methods[method]];
}

What I tried :

  • Replace loading state by using useRef() hook : const loading = useRef(false); and update loading.current before and after fetch, but this didn't work, in my component where I called the custom hooke, the loading value received isn't updated
  • Call fetch functions in a useEffect with a 'data' state dependency (const [data, setData] = useState({})), and returning setData instead of fetch functions, didn't work either, the setData made an infinite loop on some cases (in a react-rooter loader function I had an infinite loop, but not on en event submit event)
2
  • 1
    There are a lot of problems with this code: Promise constructor antipattern, returning from a Promise constructor callback, performing side effects in a React hook without wrapping them in useEffect, not memoizing complex assignments, etc etc. I would recommend that you look at some prior art (google useFetch for example) and try to deepen your understanding of Javascript and React before re-attempting this. If you need it to solve a problem in your codebase, use an off-the-shelf solution, there are plenty of libraries in this space. Commented Jun 1, 2023 at 15:34
  • @JaredSmith Thanks for pointing errors in this code, I'll look for it. But it doesn't answer the question. I should have simplified a lot this custom hook in my post to focus only on my loading state problem. I've googled and seen many useFetch custom hook, but none that handled loading state as wanted. I don't knoy if it's correct to edit my post or create another with a more basic example of what i'm searching Commented Jun 1, 2023 at 15:46

1 Answer 1

0

Got it working after simplifying my custom hook, my infinite loop bug didn't come from my setLoading() calls :

import { useState } from "react";

export default function useApi({method, url}) {

    const [loading, setLoading] = useState(false);

    const methods = {
        get: function (data = {}) {
            return new Promise((resolve, reject) => {
                setLoading(true);
                const params = new URLSearchParams(data);
                const queryString = params.toString();
                const fetchUrl = url + (queryString ? "?"+queryString : "");
                fetch(fetchUrl, {
                    method: "get",
                    headers: {
                        "Content-Type": "application/json",
                        "Accept": "application/json",
                    },
                })
                .then(response => response.json())
                .then(data => {
                    if( !data ){
                        setLoading(false);
                        return reject(data);
                    }
                    setLoading(false);
                    resolve(data);
                })
                .catch(error => {
                    setLoading(false);
                    console.error(error);
                });
            })
        },
        post :  function (data = {}) {
            return new Promise((resolve, reject) => {
                setLoading(true);
                fetch(url, {
                    method: "post",
                    headers: {
                        "Content-Type": "application/json",
                        "Accept": "application/json",
                    },
                    body: JSON.stringify(data)
                })
                .then(response => response.json())
                .then(data => {
                    if( !data ){
                        setLoading(false);
                        return reject(data);
                    }
                    setLoading(false);
                    resolve(data);
                })
                .catch(error => {
                    setLoading(false);
                    console.error(error);
                });
            })
        }
    }

    if ( !(method in methods) ) {
        throw new Error("Incorrect useApi() first parameter 'method'")
    }

    return [loading, methods[method]];
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.