1

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

  • I'm curious why you're using `useEffect` in `useMyFetch`. Is it that you want to memoize the response by the `url` and `options`? That means you assume the response doesn't change over time, is that really true? – T.J. Crowder Sep 29 '19 at 09:17
  • go the link and show the solution https://repl.it/repls/CumbersomeCruelCopyright – Mohammed Al-Reai Sep 29 '19 at 09:27
  • offtopic: your `useMyFetch` need to include [some cleanup](https://medium.com/hackernoon/avoiding-race-conditions-when-fetching-data-with-react-hooks-220d6fd0f663) in `useEffect` or you get race conditions – skyboyer Sep 29 '19 at 10:03

3 Answers3

1

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.

Titus
  • 22,031
  • 1
  • 23
  • 33
  • 1
    @T.J.Crowder The OP mentioned that adding `options` to the array passed as the second parameter to `useEffect` is the problem. – Titus Sep 29 '19 at 09:53
0

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;
Mohammed Al-Reai
  • 2,344
  • 14
  • 18
0

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).

skyboyer
  • 22,209
  • 7
  • 57
  • 64
  • It's not clear to me what the OP really wants to do, but **if** comparing objects is part of the answer, you'll want to go a bit further than the above. `JSON.stringify({a: 1, b: 2}) !== JSON.stringify({b: 2, a: 1})` per specified behavior. You'd need to do something like `JSON.stringify(Object.fromEntries( Object.entries(o).sort(([[k1]], [[k2]]) => k1.localeCompare(k2)) ))` and at that point, you're better off just writing an object comparison function. – T.J. Crowder Sep 29 '19 at 09:44
  • @T.J.Crowder sure, you're right. I have described custom comparator usage as well. By the way, while using literal object construction there is no chance for providing keys in different ordering. – skyboyer Sep 29 '19 at 09:49
  • I don't understand what you mean by your final sentence there. – T.J. Crowder Sep 29 '19 at 09:54
  • @T.J.Crowder having call `useMyFetch(url, {a: 1, b: 2})` there is no chance to get `JSON.stringify(options)` returns different result – skyboyer Sep 29 '19 at 10:01
  • Yes, but you can call `useMyFetch(url, {a: 1, b: 2})` in one place and `useMyFetch(url, {b: 2, a: 1})` in another. – T.J. Crowder Sep 29 '19 at 10:03