Sébastien D
Sébastien D

Reputation: 31

React useEffect and async fetch

Here is my code:


const useMyFetch = (url, options) => 
{
  const [response, setResponse]   = React.useState(null);

  React.useEffect(() => 
  {
    console.log("going to fetch ", url);

    fetch(url, options).then(async function(response) 
    {
      var json = await response.json();
      setResponse(json.message);
    });

  }, [ url ]); 

  return response;
};



function Example() 
{
  const res = useMyFetch("https://dog.ceo/api/breeds/image/random", { method: 'GET' });

  if (!res) 
  {
    return <div>loading...</div>
  }

  return <img src={res} alt="an image" />;
}


It looks that everything is fine... except when I replace the second argument of useEffect from [ url ] to [ url, options ]. When 'options' is there, we're entering in the well known infinite loop... however it's logical to have it in this array. What's wrong here? Thanks

Upvotes: 1

Views: 1260

Answers (3)

skyboyer
skyboyer

Reputation: 23705

As @Titus already mentioned it's because { method: 'GET' } is referentially different each new render. But I believe moving that out the component is not flexible enough in real life. Need to pass token to headers or any other dynamic calculation is rather expected requirement, right?

Option 1. We may use JSON.stringify to pass parameter object as a string:

function useMyFetch(url, options) {
  useEffect(() => {
  }, [url, JSON.stringify(options)])
} 

Option 2. We may use useRef to store previous props and use custom comparison:

function useMyFetch(url, options) {
  const prevOptions = useRef(null);
  useEffect(() => {
   ...
   prevOptions.current = options;
  }, [url, customCompare(options, prevOptions.current)])
} 

Option 3. And finally me may create custom hook that would return referentially same object if all nested properties are equal:

function useObject(obj) {
  const prevObj = useRef(obj);
  const isEqual = _.isEqual(prevObj.current, obj);
  useEffect(() => {
    prevObj.current = obj;
  }, [isEqual]);
  return isEqual ? prevObj.current : obj;
}

and use it later as

function Example() 
{
  const requestOptions = useObject({ method: 'GET' });
  const res = useMyFetch("https://dog.ceo/api/breeds/image/random", requestOptions);

  if (!res) 
  {
    return <div>loading...</div>
  }

  return <img src={res} alt="an image" />;
}

Option 4. Most straightforward way is to decompose object options into primitive values:

function useMyFetch(url, options) {
  const {method, headers: {contentType} = {} , ...rest } = options;
  useEffect(() => {
  }, [url, method, contentType, JSON.stringify(rest)]);
}

I believe while this method is more verboose than others above, it's slightly faster especially if rest typically is empty(no extra headers).

Upvotes: 0

Mohammed Al-Reai
Mohammed Al-Reai

Reputation: 2786

look the URL for testing show the image it should return the setResponse what you want

const useMyFetch = (url, options) => 
{
  const [response, setResponse]   = React.useState(null);

  React.useEffect(() => 
  {
    console.log("going to fetch ", url);

    fetch(url, options).then(async (response)=> 
    {
      var json = await response.json();
       return setResponse(json.message);
    });

  }, [ url ]); 

  return response;
};



function Example() 
{
  const res = useMyFetch("https://dog.ceo/api/breeds/image/random", { method: 'GET' });

  if (!res) 
  {
    return <div>loading...</div>
  }

  return <img src={res} alt="an image" />;
}


export default Example;

Upvotes: 0

Titus
Titus

Reputation: 22474

Define { method: 'GET' } as a constant object so that the options parameter will be always the same, here is an example:

const options = { method: 'GET' };
function Example() {
  const res = useMyFetch("https://dog.ceo/api/breeds/image/random", options);
  ...
}

Otherwise, options will be considered as changed every time the useMyFetch is called because { method: 'GET' } === { method: 'GET' } is false.

Upvotes: 1

Related Questions