Ghosty Frosty
Ghosty Frosty

Reputation: 168

React useEffect dependencies invalidation

I am currently working on a react project and I ran into the need of manually triggering an useEffect. For that, I am using an extra dependency, a "valid" flag, which if true will also trigger a fetch. The thing is, the fetch also has parameters that it depends on, so now my effect depends on [page, limit, title, valid]. The problem with this is that I cannot simply let it this way, because then I wouldn't know when I should refetch. What would be a good solution to allow refetch on parameter changes but also when manually changing the "valid" param? For now, I am doing the following:

export function useJobOpenings(page: number, limit: number, title: string): UseJobOpeningsResponse {
    const [jobOpenings, setJobOpenings] = useState<JobOpeningDTO[]>([]);
    const [fetching, setFetching] = useState(false);
    const [error, setError] = useState<unknown>();
    const [valid, setValid] = useState(false);

    useEffect(() => {
        setValid(false);
    }, [limit, page, title]);

    // eslint-disable-next-line react-hooks/exhaustive-deps
    useEffect(fetchJobOpenings, [valid]);

    return { jobOpenings, fetching, error, invalidate: (): void => setValid(false) };

    function fetchJobOpenings(): (() => void) | void {
        if (valid) return;
        let mounted = true;

        asyncFetch();

        return (): void => {
            mounted = false;
        };

        async function asyncFetch(): Promise<void> {
            setFetching(true);
            try {
                const _jobOpenings = await getAllJobOpenings({ title, page, limit });
                if (!mounted) return;

                setJobOpenings(_jobOpenings.data);
                setFetching(false);
                setValid(true);
            } catch (err) {
                if (!mounted) return;
                setError(err);
                setFetching(false);
                setValid(true);
            }
        }
    }
}

The problem with this is that it seems really counterintuitive to update the "valid" flag based on the other dependencies and then use only that flag in the effect that actually does the fetching, since it also triggers the exhaustive deps elsint rule, even though it seems pretty obvious, hopefully it is correct as well, that the valid flag already depends on the 3 other parameters.

Upvotes: 0

Views: 570

Answers (2)

Raff
Raff

Reputation: 11

I'm not entirely sure what purpose does your code serve, but I've had a similar use case when I wanted to refetch data once I knew it was changed. Here is a solution I came up with. I know it's written in a different manner than your code it, but hope it helps.

It's a general purpose component used to fetch data from an endpoint, which url and data you specify as arguments.

export const Resource = ({ path, data, children, ...props }) => {
    const [loading, setLoading] = useState(true);
    const [payload, setPayload] = useState([]);
    const [refreshVar, setRefreshVar] = useState(false);

    const refresh = () => {
        setRefreshVar(v => !v)
    }

    useEffect(() => {
        let func = data ? axios.post : axios.get;
        func(path, data)
            .then(res => {
                setPayload(res.data)
                setLoading(false)
            })
            .catch(console.log)
    }, [path, refreshVar, data]);

    return children({ loading, payload, refresh, ...props });
};

Then you have a component which you can use this way:

<Resource path={api.yourEndpoint}>
    {({ payload, loading, refresh }) => {
        if (!loading && payload.length === 0) {
            return <EmptyComponent />;
        }

        return (
            <div>
                {payload.map((record, index) => (
                    <div>{record.value}</div>
                ))}

               <button onClick={refresh}>
                   Refetch Data
               </button>
            </div>
        );
    }}
</Resource>

From this point you could create a component instead of the sample div passing down the arguments, validating the fetched data inside and calling the refresh function if necessary.

Upvotes: 1

Ghosty Frosty
Ghosty Frosty

Reputation: 168

If anyone comes across this, the idea would be to not count on valid for the "true" value but instead to care only about the fact that it can change.

export function useJobOpenings(page: number, limit: number, title: string): UseJobOpeningsResponse {
    const [jobOpenings, setJobOpenings] = useState<JobOpeningDTO[]>([]);
    const [fetching, setFetching] = useState(false);
    const [error, setError] = useState<unknown>();
    const [valid, setValid] = useState(false);

    useEffect(fetchJobOpenings, [limit, page, title, valid]);

    return { jobOpenings, fetching, error, invalidate: (): void => setValid((old) => !old) };

    function fetchJobOpenings(): (() => void) | void {
        let mounted = true;

        asyncFetch();

        return (): void => {
            mounted = false;
        };

        async function asyncFetch(): Promise<void> {
            setFetching(true);
            try {
                const _jobOpenings = await getAllJobOpenings({ title, page, limit });
                if (!mounted) return;

                setJobOpenings(_jobOpenings.data);
                setFetching(false);
            } catch (err) {
                if (!mounted) return;
                setError(err);
                setFetching(false);
            }
        }
    }
}

Notice that inside the effect there's no check whether valid is true or false, nor does it set it to any value, since the only thing that matters is the fact that the value can change and trigger the effect.

Upvotes: 0

Related Questions