skip to Main Content

I made custom useFetch hook that is responsible for sending request to the API.
Hook returns object with isLoading state, error state and function that triggers fetch request.
Problem is when i use it in component and i trigger sendRequest function (which is returned by the hook), component doesent get latest errorState provided by the hook.
I kinda understand where is the problem but still cannot find solution.

Here is useFetch hook

const useFetch = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const sendRequest = async (
    url: string,
    requestConfig?: RequestInit | undefined
  ) => {
    setIsLoading(true);
    setError(null);

    try {
      const response = await fetch(url, {
        method: requestConfig ? requestConfig.method : "GET",
        headers: requestConfig ? requestConfig.headers : {},
        body: requestConfig ? JSON.stringify(requestConfig.body) : null,
      });

      if (!response.ok) throw new Error("Request failed!");

      const data = response.json();

      return data;
    } catch (error: any) {
      setError(error.message);
    } finally {
      setIsLoading(false);
    }
  };

  return {
    isLoading,
    error,
    sendRequest,
  };
};

Here is component that use hook

type ContactProps = {
  contact: TContact;
  setContacts: React.Dispatch<React.SetStateAction<TContact[]>>;
};

function Contact({ contact, setContacts }: ContactProps) {
  const { isLoading, error, sendRequest } = useFetch();

  const deleteContactHandler = useCallback(async () => {
    const response = await sendRequest(`${BASE_API_URL}/users/${contact.id}`, {
      method: "DELETE",
    });

    console.log(response);
    console.log(error);

    if (!error) {
      setContacts((prevContacts) => {
        return prevContacts.filter((item) => item.id !== contact.id);
      });
    }
  }, []);

  return (
    <li className={classes["contact-item"]}>
      <figure className={classes["profile-picture-container"]}>
        <img
          src={contact.profilePicture}
          alt={contact.name}
          className={classes["profile-picture"]}
        ></img>
      </figure>

      <div className={classes["contact-info"]}>
        <p>{contact.name}</p>
        <p>{contact.phone}</p>
        <p>{contact.email}</p>
      </div>

      <div className={classes["contact-actions"]}>
        <Button
          className={classes["actions-btn"]}
          onClick={() => console.log("Dummy!!!")}
        >
          <BsPencilFill />
        </Button>

        <Button
          className={classes["actions-btn"]}
          onClick={deleteContactHandler}
        >
          <BsFillTrashFill />
        </Button>
      </div>
    </li>
  );
}```








2

Answers


  1. Your deleteContactHandler is wrapped in a useCallback which means that it will not get updated when state changes. If you have a linter this will probably we mentioned. Something like

    React Hook useCallback has missing dependencies: 'error' and 'sendRequest'
    

    So to fix this you can add the dependencies.

    const deleteContactHandler = useCallback(async () => {
      const response = await sendRequest(
        `https://jsonplaceholder.typicode.com/todos`
      );
    
      console.log(response);
      console.log(error);
    
      if (!error) {
        setContacts((prevContacts) => {
          return response.data;
        });
      }
    }, [error, sendRequest]);
    

    But this will still not fix your problem since the error has still the previous value of null from when the function got called.

    I suggest you do some refactoring where you add a data state to your hook. And pass the url and method when you initialize the hook.

    const useFetch = (url: string, requestConfig?: RequestInit | undefined) => {
      const [isLoading, setIsLoading] = useState(false);
      const [error, setError] = useState<string | null>(null);
      const [data, setData] = useState<any>(null);
    
      const sendRequest = async () => {
        setIsLoading(true);
        setError(null);
    
        try {
          const response = await fetch(url, {
            method: requestConfig ? requestConfig.method : "GET",
            headers: requestConfig ? requestConfig.headers : {},
            body: requestConfig ? JSON.stringify(requestConfig.body) : null,
          });
    
          if (!response.ok) throw new Error("Request failed!");
    
          const data = await response.json();
    
          setData(data);
        } catch (error: any) {
          setError(error.message);
        } finally {
          setIsLoading(false);
        }
      };
    
      return {
        isLoading,
        error,
        data,
        sendRequest,
      };
    };
    

    Which you can use like so

    const { isLoading, error, data, sendRequest } = useFetch(
      `${BASE_API_URL}/users/${contact.id}`,
      {
        method: "DELETE",
      }
    );
    

    Of course you’ll want to set your setContacts with the new data. For that you can use a useEffect.

    useEffect(() => {
      setContacts(data);
    }, [data]);
    

    Also worth mentioning is that you’re not awaiting the response.json().

    const data = await response.json();
    

    Here is a live preview.

    Login or Signup to reply.
  2. Functionality that affects state should be wrapped in a useCallback hook.

    Here is a simplified working version of your example. You can see the error occur by changing result.ok to false in mockFetch.

    import React, { useCallback, useEffect, useState } from "react";
    import "./App.css";
    
    const mockFetch = async () => {
      return { ok: true, json: () => ["Contact 1", "Contact 2"] };
    };
    
    const useFetch = () => {
      const [isLoading, setIsLoading] = useState(false);
      const [error, setError] = useState<string | null>(null);
      const [response, setResponse] = useState<string[] | null>(null);
    
      const sendRequest = useCallback(async () => {
        setIsLoading(true);
        setError(null);
    
        try {
          const response = await mockFetch();
    
          if (!response.ok) throw new Error("Request failed!");
    
          const data = await response.json();
    
          setResponse(data);
        } catch (error: any) {
          setError(error.message);
        } finally {
          setIsLoading(false);
        }
      }, []);
    
      return {
        isLoading,
        error,
        sendRequest,
        response,
      };
    };
    
    type ContactProps = {
      contact: any;
      setContacts: React.Dispatch<React.SetStateAction<string[]>>;
    };
    
    function Contact({ contact, setContacts }: ContactProps) {
      const { isLoading, error, sendRequest, response } = useFetch();
    
      const handleOnClick = useCallback(() => {
        sendRequest();
      }, [sendRequest]);
    
      useEffect(() => {
        if (isLoading) return;
    
        if (error) {
          alert("ERROR");
          return;
        }
    
        if (response) setContacts(response);
      }, [contact.id, error, isLoading, response, setContacts]);
    
      return <button onClick={handleOnClick}>Do something</button>;
    }
    
    function App() {
      const [contacts, setContacts] = useState([""]);
    
      return (
        <div>
          Contacts: {contacts}
          <Contact contact={contacts} setContacts={setContacts} />
        </div>
      );
    }
    
    export default App;
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search