1

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>
  );
}```








saksonia
  • 11
  • 2

2 Answers2

0

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.

RubenSmn
  • 4,091
  • 2
  • 5
  • 24
  • First of all, thanks for mentioning linter, this is my first time working without create-react-app config so im kinda lost. I dont understand why do i need to add data state. Until now i only needed hooks like this to perform get request and there i used state for data but it looks like this one is bit more advanced for my knowledge. Will definitely need to read up a bit more before realizing fully whats happening here. Thanks you for your time, cheers! – saksonia Feb 25 '23 at 16:28
0

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;
Evert
  • 93,428
  • 18
  • 118
  • 189
  • Thank you for your help. I still don't fully understand whats wrong but i guess this goes beyond my knowledge about react. Need to do some more reading on re-rendering i guess. Anyway, thanks alot! – saksonia Feb 25 '23 at 16:24