thoroughlyConfused
thoroughlyConfused

Reputation: 21

How to synchronously serialize api calls in react?

I have a task that requires fetching api data, with the constraint of only one outstanding api request at a time. Must receive a response, or time out, before issuing the next one. Since fetch (or axios) returns a promise, I can’t figure out how to wait for each promise to fulfill before issuing the next fetch.

I'm handed a large array of api url's that must all be resolved in this one-at-a-time manner before continuing.

I’m using create-react-app’s bundled dev server, and Chrome browser.

Curiously, accomplishing this via a node script is easy, because ‘await fetch’ actually waits. Not so in my browser environment, where all the fetch requests blast out at once, returning promises along the way.

Here’s a simple loop that results in the desired behavior as a node script. My question is how to achieve this one-outstanding-request-at-a-time synchronous serialization in the browser environment?

const fetchOne = async (fetchUrl) => {
    try {
        const response = await fetch(fetchUrl, { // Or axios instead    
            "headers": {
                'accept': 'application/json',
                'X-API-Key': 'topSecret'
            },
            'method': 'GET'
        })
        const data = await response.json();
        if (response.status == 200) {
            return (data);
        } else {
            // error handling
        }
    } catch(error) {
        // different error handling
    } 
}

const fetchAllData = async (fetchUrlArray) => {
    let fetchResponseDataArray = new Array();
    let fetchResponseDataObject = new Object(/*object details*/);
    for (var j=0; j<fetchUrlArray.length; j++) { // or forEach or map instead
        // Node actually synchronously waits between fetchOne calls,
        //   but react browser environment doesn't wait, instead blasts them all out at once.
        // Question is how to achieve the one-outstanding-request-at-a-time synchronous
        //   serialization in the browser environment?
        fetchResponseDataObject = await fetchOne(fetchUrlArray[j]);
        fetchResponseDataArray.push(fetchResponseDataObject);
    }
    return(fetchResponseDataArray);
}

Upvotes: 1

Views: 1373

Answers (1)

jsejcksn
jsejcksn

Reputation: 33701

If there's a problem, it's with code you haven't shown (perhaps in one of your components, or maybe in your project configuration).

Here's an runnable example derived from the problem you described, which mocks fetch and an API, showing you how to iterate each network request synchronously (and handle potential errors along the way):

Note, handling potential errors at the boundaries where they might occur is a better practice than only having a top level try/catch: by doing so, you can make finer-grained decisions about what to do in response to each kind of problem. Here, each failed request is stored as [url, error] in a separate array so that you can programmatically make decisions if one or more requests failed. (Maybe you want to retry them in a subsequent step, or maybe you want to show something different in the UI, etc.). Note, there's also Promise.allSettled(), which might be useful to you now or in the future.

<div id="root"></div><script src="https://unpkg.com/[email protected]/umd/react.development.js"></script><script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script><script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">

const {useEffect, useState} = React;

const successChance = {
  fetch: 0.95,
  server: 0.95,
};

function mockApi (url, chance = successChance.server) {
  // Simulate random internal server issue
  const responseArgs = Math.random() < chance
    ? [JSON.stringify({time: performance.now()}), {status: 200}]
    : ['Oops', {status: 500}];
  return new Response(...responseArgs);
}

function mockFetch (requestInfo, _, chance = successChance.fetch) {
  return new Promise((resolve, reject) => {
    // Simulate random network issue
    if (Math.random() > chance) {
      reject(new Error('Network error'));
      return;
    }
    const url = typeof requestInfo === 'string' ? requestInfo : requestInfo.url;
    setTimeout(() => resolve(mockApi(url)), 100);
  });
}

// Return an object containing the response if successful (else an Error instance)
async function fetchOne (url) {
  try {
    const response = await mockFetch(url);
    if (!response.ok) throw new Error('Response not OK');
    const data = await response.json();
    return {data, error: undefined};
  }
  catch (ex) {
    const error = ex instanceof Error ? ex : new Error(String(ex));
    return {data: undefined, error};
  }
}

async function fetchAll (urls) {
  const data = [];
  const errors = [];

  for (const url of urls) {
    const result = await fetchOne(url);
    if (result.data) data.push([url, result.data]);
    else if (result.error) {
      // Handle this however you want
      errors.push([url, result.error]);
    }
  }

  return {data, errors};
}

function Example () {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);

      try {
        const {data, errors} = await fetchAll([
          'https://my.url/api/0',
          'https://my.url/api/1',
          'https://my.url/api/2',
          'https://my.url/api/3',
          'https://my.url/api/4',
          'https://my.url/api/5',
          'https://my.url/api/6',
          'https://my.url/api/7',
          'https://my.url/api/8',
          'https://my.url/api/9',
        ]);
        setData(data);
      }
      catch (ex) {
        console.error(ex);
      }

      setLoading(false);
    };
    fetchData();
  }, []);

  return (
    <div>
      <div>Loading: {loading ? '...' : 'done'}</div>
      <ul>
        {
          data.map(([url, {time}]) => (<li
            key={url}
            style={{fontFamily: 'monospace'}}
          >{url} - {time}</li>))
        }
      </ul>
    </div>
  );
}

ReactDOM.render(<Example />, document.getElementById('root'));

</script>

Upvotes: 3

Related Questions