Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Hook Dependencies - Generic Fetch Hook

I've followed many tutorials for how to set up my own custom generic useFetch hook. What I came up with works well, but it is breaking some Rules of Hooks. Mostly, it doesn't use the "correct" set of dependencies.

The generic hook accepts a url, options, and dependencies. Setting the dependencies up as all three creates an infinite refresh loop, even though the dependencies aren't changing.

// Infinite useEffect loop - happy dependencies
const UseRequest: <T>(url: string, options?: Partial<UseRequestOptions> | undefined, dependencies?: any[]) => UseRequestResponse<T>
 = <T>(url: string, options: Partial<UseRequestOptions> | undefined = undefined, dependencies: any[] = []): UseRequestResponse<T> => {
    const [data, setData] = useState<T | undefined>();
    const [loading, setLoading] = useState<boolean>(false);
    const [error, setError] = useState<UseRequestError | undefined>();

    useEffect(() => {
        let ignore = false;
        (async () => {
            try {
                setLoading(true);
                const response = await (options ? fetch(url) : fetch(url, options))
                    .then(res => res.json() as Promise<T>);
                if (!ignore) setData(response);
            } catch (err) {
                setError(err);
            } finally {
                setLoading(false);
            }
        })();
        return (() => { ignore = true; });
    }, [url, options, dependencies]);
    return { data, loading, error };
}

I've found that it works as expected if I omit the options from dependencies (which sort of makes sense as we don't expect this deep object to change in a way we should monitor) and spread the incoming dependencies. Of course, both of these changes break the "Rules of Hooks."

// Working - mad dependencies
const UseRequest: <T>(url: string, options?: Partial<UseRequestOptions> | undefined, dependencies?: any[]) => UseRequestResponse<T>
 = <T>(url: string, options: Partial<UseRequestOptions> | undefined = undefined, dependencies: any[] = []): UseRequestResponse<T> => {
    const [data, setData] = useState<T | undefined>();
    const [loading, setLoading] = useState<boolean>(false);
    const [error, setError] = useState<UseRequestError | undefined>();

    useEffect(() => {
        let ignore = false;
        (async () => {
            try {
                setLoading(true);
                const response = await (options ? fetch(url) : fetch(url, options))
                    .then(res => res.json() as Promise<T>);
                if (!ignore) setData(response);
            } catch (err) {
                setError(err);
            } finally {
                setLoading(false);
            }
        })();
        return (() => { ignore = true; });
    }, [url, ...dependencies]);
    return { data, loading, error };
}

...which I then use like

export const GetStuff: () => UseRequestResponse<Stuff[]> & { refresh: () => void } = () => {
    const { appToken } = GetAppToken();
    const [refreshIndex, setRefreshIndex] = useState(0);
    return {
        ...UseRequest<Stuff[]>('https://my-domain.api/v1/stuff', {
            method: 'GET',
            headers: {
                'Authorization': `Bearer ${appToken}`
            }
        }, [appToken, refreshIndex]),
        refresh: () => setRefreshIndex(refreshIndex + 1),
    };
};

Notice, the only change between the working and broken states was:

}, [url, options, dependencies]);

...to:

}, [url, ...dependencies]);

So, how could I possibly rewrite this to follow the Rules of Hooks and not fall into an infinite refresh loop?

Here is the full code for useRequest with the defined interfaces:

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

const UseRequest: <T>(url: string, options?: Partial<UseRequestOptions> | undefined, dependencies?: any[]) => UseRequestResponse<T>
 = <T>(url: string, options: Partial<UseRequestOptions> | undefined = undefined, dependencies: any[] = []): UseRequestResponse<T> => {
    const [data, setData] = useState<T | undefined>();
    const [loading, setLoading] = useState<boolean>(false);
    const [error, setError] = useState<UseRequestError | undefined>();

    useEffect(() => {
        let ignore = false;
        (async () => {
            try {
                setLoading(true);
                const response = await (options ? fetch(url) : fetch(url, options))
                    .then(res => res.json() as Promise<T>);
                if (!ignore) setData(response);
            } catch (err) {
                setError(err);
            } finally {
                setLoading(false);
            }
        })();
        return (() => { ignore = true; });
    }, [url, ...dependencies]);
    return { data, loading, error };
}

export default UseRequest;

export interface UseRequestOptions {
    method: string;
    mode: 'cors', // no-cors, *cors, same-origin
    cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
    credentials: 'same-origin', // include, *same-origin, omit
    headers: {
        [prop: string]: string;
    },
    redirect: string, // manual, *follow, error
    referrerPolicy: string, // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
    body: string | { [prop: string]: any };
    [prop: string]: any;
};

export interface UseRequestError {
    message: string;
    error: any;
    code: string | number;
    [prop: string]: any;
}

export interface UseRequestResponse<T> {
    data: T | undefined;
    loading: boolean;
    error: Partial<UseRequestError> | undefined;
}
like image 681
WebWanderer Avatar asked Oct 16 '20 16:10

WebWanderer


People also ask

How do you fetch data using React Hooks?

Put the fetchData function above in the useEffect hook and call it, like so: useEffect(() => { const url = "https://api.adviceslip.com/advice"; const fetchData = async () => { try { const response = await fetch(url); const json = await response. json(); console. log(json); } catch (error) { console.

How do I fix React hook useEffect has missing dependencies?

The warning "React Hook useEffect has a missing dependency" occurs when the useEffect hook makes use of a variable or function that we haven't included in its dependencies array. To solve the error, disable the rule for a line or move the variable inside the useEffect hook.

What are dependencies in React Hooks?

What is a dependency array. Dependency arrays are a concept that is tightly coupled to hooks in React (thus also to function components). Some hooks, like useEffect and useCallback have 2 arguments. The first one is a callback (a function), and the second one is the dependency array.


1 Answers

That's because you recreate a new array on each render. In fact the whole dependency makes no sense since you never use it inside the effect.

You could equally rely on the options object, which has changing headers. But since the object also gets recreated on each render you have to memoize it first:

export const GetStuff: () => UseRequestResponse<Stuff[]> & { refresh: () => void } = () => {
    const { appToken } = GetAppToken();
    const [refreshIndex, setRefreshIndex] = useState(0);

    const options = useMemo(() => ({
        method: 'GET',
        headers: {
            'Authorization': `Bearer ${appToken}`
        }
    }), [appToken, refreshIndex])

    return {
        ...UseRequest<Stuff[]>('https://my-domain.api/v1/stuff', options),
        refresh: () => setRefreshIndex(refreshIndex + 1),
    };
};

Then, instead of relying on the refresh index to trigger a refresh you could have the useRequest() hook return a refresh function, which internally also calls that function in the effect (instead of putting the load logic in the effect itself, it just calls that function). This way you follow the rules even better, since the useMemo never actually depends on the refresh index so it shouldn't be in the dependencies.

like image 161
Mordechai Avatar answered Nov 01 '22 22:11

Mordechai