5

I would like to do multiple fetch requests with React/JavaScript, I have the following attempt which kind of works:

const fetchAll = async () => {
    Promise.all([
        await fetch('/api/...'),
        await fetch('/api/...')
    ]).then(links => {
        const response1 = links[0];
        const response2 = links[1];

        timeData = response1.json();
        functionData = response2.json();
    })
}

But I would like to do it this way, because that seems more useful. I would like to use useEffect and useState and load the data of the different APIs in different arrays in useState if that's possible. Here is an example:

const [data, setData] = useState([]);
useEffect(() => {
        fetch("/api/..")
            .then((response) => response.json())
            .then((r) => {
                setData(r);
            });
    }, []);

Is there a way to do this for multiple requests and save the data in different arrays so I can access them later on?

Phil
  • 157,677
  • 23
  • 242
  • 245
salteax1
  • 67
  • 1
  • 7

3 Answers3

3

If I'm understanding your question correctly, then I think you want something like this:

const [resp1, setResp1] = useState();
const [resp2, setResp2] = useState();

useEffect(() => {
    Promise.all([
        fetch('/api/...'),
        fetch('/api/...')
    ]).then(links => {
        const response1 = links[0];
        const response2 = links[1];
        
        setResp1(response1);
        setResp2(response2);
    })
}, [/*dependency array*/])

Here we leverage a useEffect to make your simultaneous fetches; we don't really need nor want to use await because we want them to run simultaneously and the Promise.all will only resolve when all the promises in the array resolve. In the .then of the Promse.all, you can parse out the responses just as you did in your original example and set them in state hooks you've defined earlier in your component. You'd probably want to make sure that you have an appropriately-defined dependency array passed as a second arg to your useEffect, in order to make sure that it only executes under the conditions in which you want it to. This would work for some limited number of fetches-- for something dynamic or with very large numbers of fetches, this approach would probably be less ideal, and you'd want something that scaled better.

Alexander Nied
  • 12,804
  • 4
  • 25
  • 45
  • Not much of an explanation.... – Heretic Monkey Jun 14 '22 at 14:49
  • @HereticMonkey - yeah, that's a fair point- I've updated the post with some explanatory detail. – Alexander Nied Jun 14 '22 at 15:48
  • @AlexanderNied thanks for the help but this leads to an infinte loop. Do you have any clue why that happens? – salteax1 Jun 15 '22 at 03:05
  • @salteax1 - this is probably because you are missing a dependency array as the second arg to `useEffect`; I mentioned this in the text of the answer but didn't include it in my code example. I've updated the code example to include an empty array as the second arg-- this will cause the function passed to `useEffect` to only run once when the component mounts and not on any subsequent re-renders. You'll want to evaluate if this is appropriate in your case. If this is _not_ the source of your re-renders, you might need to provide more context, or maybe a new question. Good luck; happy coding! – Alexander Nied Jun 15 '22 at 03:28
  • @AlexanderNied Could you explain what an dependency array is? Should i define an array with all the values the fetched array has? I also can provide more context but im not sure what exactly you need – salteax1 Jun 15 '22 at 03:41
  • Heres what has to go into the array: "quarterNumber" ,"year". How would i define an empty array like this to be a dependency array? – salteax1 Jun 15 '22 at 03:48
2

This can be done nicely by

  1. Creating an array of URLs to resolve
  2. Iterating the returned array data
  3. Passing that data into your setters

For example, create some helper functions in some module

// fetch-helpers.js

// performs a request and resolves with JSON
export const fetchJson = async (url, init = {}) => {
  const res = await fetch(url, init);
  if (!res.ok) {
    throw new Error(`${res.status}: ${await res.text()}`);
  }
  return res.json();
};

// get JSON from multiple URLs and pass to setters
export const fetchAndSetAll = async (collection) => {
  // fetch all data first
  const allData = await Promise.all(
    collection.map(({ url, init }) => fetchJson(url, init))
  );

  // iterate setters and pass in data
  collection.forEach(({ setter }, i) => {
    setter(allData[i]);
  });
};

and in your component...

import { useEffect, useState } from "react";
import { fetchAndSetAll } from "./fetch-helpers";

export const MyComponent = () => {
  // initialise state to match the API response data types
  const [timeData, setTimeData] = useState([]);
  const [functionData, setFunctionData] = useState([]);

  useEffect(() => {
    fetchAndSetAll([
      {
        url: "/api/...",
        setter: setTimeData,
      },
      {
        url: "/api/...",
        setter: setFunctionData,
      },
    ]).catch(console.error);
  }, []);

  return <>{/* ... */}</>;
};
Phil
  • 157,677
  • 23
  • 242
  • 245
  • So if i put the code below into my component, where would i put the upper code? I have all the code in the component so this quite confuses me. EDIT: On top, i see – salteax1 Jun 15 '22 at 03:57
  • 1
    @salteax1 you can put it anywhere you want but I'd recommend a separate module (aka _file_) as it makes testing easier – Phil Jun 15 '22 at 04:00
  • Thanks, that kinda works now but when i try to access one of the arrays in my return where i got all the html code, i get the error the timeData for example is undefined? Is this because the useEffect is getting executed after the return? If so how could i fix this? – salteax1 Jun 15 '22 at 04:03
  • 1
    @salteax1 that's a bit hard to tell without your code or knowing what data type is returned from your API calls. If they're arrays (as shown by the initial state values), you can safely map them if they're empty of not. Just make sure you initialise them correctly, eg `useState([])` – Phil Jun 15 '22 at 04:05
  • Thanks, it works i just made some typos. Thanks for your help :) – salteax1 Jun 15 '22 at 04:12
  • 1
    @salteax1 I've added a small update that will result in fewer re-renders. It just fetches all data before passing it into the setters – Phil Jun 15 '22 at 04:18
  • this leads to the error "getJson is undefined" is this a me error or is this some error in your code? – salteax1 Jun 15 '22 at 04:34
  • 1
    Oops, just a typo. I renamed the function but didn't fix it everywhere – Phil Jun 15 '22 at 04:49
1

Try this,

As of today, fetch is now implemented in all the latest version of the major browsers, with the exception of IE11, a wrapper could still be useful unless you use a polyfill for it.

Then, taking advantage of newer and now more stable javascript features like destructuring and async/await, you might be able to use a similar solution to the same problem (see the code below).

I believe that even though at first sight may seem a little more code, is actually a cleaner approach. Hope it helps.

try {
  let [items, contactlist, itemgroup] = await Promise.all([
    fetch("http://localhost:3000/items/get"),
    fetch("http://localhost:3000/contactlist/get"),
    fetch("http://localhost:3000/itemgroup/get")
  ]);

  ReactDOM.render(
    <Test items={items} contactlist={contactlist} itemgroup={itemgroup} />,
      document.getElementById('overview');
  );
}
catch(err) {
  console.log(err);
};
Manishyadav
  • 1,361
  • 1
  • 8
  • 26