-1

I'm trying to use a useFetch custom hook on a small todolist app that I'm working on to learn React.

I don't get why my useFetch function seems to work but its inner useEffect never triggers.

I tried removing the URL from dependencies array, adding the URL as an argument of the useEffect but nothing happened: my variable [response] stays null.

Here is the code for the useFetch :

utils.js:

export function useFetch(url) {
    const [response, setResponse] = useState(null);
    const [error, setError] = useState(null);
    const [isLoading, setIsLoading] = useState(false);

    useEffect(() => {
        console.log(url);
        if (url === undefined) return;

        const fetchData = async () => {
            setIsLoading(true);
            try {
                const result = await getRequest(url);
                setResponse(result);
                setIsLoading(false);
            } catch (error) {
                setError(error);
            }
        };
        fetchData();
    }, [url]);

    return [response, setResponse, error, isLoading];
}

App.js:

import { useState, useMemo, useCallback } from 'react';
import { useFetch, postRequest, deleteRequest, getFormatedDate } from './utils';
//more imports

export default function App() {
    const [response] = useFetch('/items');
    const [titleValue, setTitleValue] = useState('');
    const [descriptionValue, setDescriptionValue] = useState('');
    const [deadlineValue, setDeadlineValue] = useState(new Date());
    const [doneFilter, setDoneFilter] = useState(0);
    const [selectedItem, setSelectedItem] = useState();
    const [showDialog, setShowDialog] = useState(false);

    const onSave = useCallback(
        async () => {
            if (titleValue) {
                let valueToSave = {};
                valueToSave.title = titleValue;
                valueToSave.status = false;
                if (descriptionValue) valueToSave.description = descriptionValue;
                valueToSave.deadline = deadlineValue instanceof Date ?  deadlineValue : new Date();
                setData((prev) => [...prev, valueToSave]);
                setTitleValue('');
                setDescriptionValue('');
                setDeadlineValue(new Date());
                try {
                    await postRequest('add', valueToSave);
                } catch (err) {
                    console.error(err);
                    throw err;
                }
            }
        },
        [descriptionValue, titleValue, deadlineValue]
    );

    const onDelete = useCallback(async (item) => {
        setData((items) => items.filter((i) => i !== item));
        try {
            await deleteRequest(item._id);
        } catch (err) {
            console.error(err);
            throw err;
        }
    }, []);

    const onModif = useCallback(async (id, field) => {
        const res = await postRequest('update/' + id, field);
        if (res.ok) setShowDialog(false);
    }, []);

    const organizedData = useMemo(() => {
        if (!response) return;
        for (let d of response) d.formatedDeadline = getFormatedDate(d.deadline);
        response.sort((a, b) => new Date(a.deadline) - new Date(b.deadline));
        if (doneFilter === 1) return response.filter((e) => e.status);
        else if (doneFilter === 2) return response.filter((e) => !e.status);
        else return response;
    }, [response, doneFilter]);

    //more code

    return (
        // jsx
)}

console.logging works just above the useEffect but never inside.

pierrepit
  • 1
  • 2
  • Do you actually get any data from ```const result = await getRequest(url)```? It might help if you share the getRequest function. – SlothOverlord Feb 06 '23 at 14:58
  • The implementation of `useFetch` seems to work well. Do you get any errors? – evolutionxbox Feb 06 '23 at 14:58
  • The hook works fine: https://stackblitz.com/edit/react-ts-cvmfto?file=useFetch.ts Please provide a reproducible example. You can add to that example until you reproduce the error. – Chris Hamilton Feb 06 '23 at 15:07
  • **NO**, the hook is broken in (current) React 18. There is no protection against setting state on an unmounted component and an error response never sets loading to false. – Mulan Feb 06 '23 at 15:14

1 Answers1

0

I cannot easily recreate your issue but I can point out some issues with your useFetch hook -

function useFetch(url) {
    const [response, setResponse] = useState(null);
    const [error, setError] = useState(null);
    const [isLoading, setIsLoading] = useState(false);

    useEffect(() => {
        console.log(url);
        if (url === undefined) return;

        const fetchData = async () => {
            setIsLoading(true);
            try {
                const result = await getRequest(url);
                setResponse(result);
                setIsLoading(false);
            } catch (error) {
                setError(error);
                // ❌ loading == true
            }
        };
        fetchData();
        // ❌ what about effect cleanup?
    }, [url]);

    return [response, setResponse, error, isLoading]; // ❌ don't expose setResponse
}

Check out Fetching Data from the react docs. Here's the fixes -

function useFetch(url) {
  const [response, setResponse] = useState(null);
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(
    () => {
      if (url == null) return;
      let mounted = true                      // ✅ component is mounted

      const fetchData = async () => {
        try {
          if (mounted) setIsLoading(true);    // ✅ setState only if mounted
          const response = await getRequest(url);
          if (mounted) setResponse(response); // ✅ setState only if mounted
        } catch (error) {
          if (mounted) setError(error);       // ✅ setState only if mounted
        } finally {
          if (mounted) setIsLoading(false);   // ✅ setState only if mounted
        }
      };

      fetchData();

      return () => {
        mounted = false // ✅ component unmounted
      }
    },
    [url]
  );

  return { response, error, isLoading }
}

When you use it, you must check for isLoading first, then null-check the error. If neither, response is valid -

function MyComponent() {
  const {response, error, isLoading} = useFetch("...")
  if (isLoading) return <Loading />
  if (error) return <Error error={error} />
  return (
    // response is valid here
  )
}

See this Q&A for a more useful useAsync hook.

Mulan
  • 129,518
  • 31
  • 228
  • 259