Search code examples
reactjsjsonreact-hooksfetch

React: Fetching inside a map and modifying a useState array


Can you guys please help me with a basic React app :) I want to read a json file:

[
    {
        "name": "Google",
        "url": "www.google.com",
        "status": "to evaluate"
    },
    {
        "name": "Bing",
        "url": "www.bing.com",
        "status": "to evaluate"
    },
    etc.
]

While reading it, I want to fetch the url to fill the status in this json file

finally, I just want to make a table that has two columns: the first is the name with the url, the second is the status

I tried this but it does not work :O

import React, { useState } from 'react'
import Table from 'react-bootstrap/Table'
import jsonData from './data/urls.json'

function ListUrls () {
    const [jsonArray, setJsonArray] = useState(jsonData)

    async function fetchData (array) {
        try {
            array.map (url => {
                const response = await fetch(url.url)
                setJsonArray(url.status = response.status)
            })
        } catch (error) {
            console.log('Error', error);
        }
        
    fetchData(jsonArray)

    return (
        <div>
            <Table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Status</th>
                    </tr>
                </thead>
                <tbody>
                    {jsonArray.map(url => (
                        <tr>
                            <td>
                                <a href={url.url} target='_blank'}>
                                    {url.name}
                                </a>
                            </td>
                            <td>
                                {url.status}
                            </td>
                        </tr>
                    ))}
                </tbody>
            </Table>
        </div>
    )
}

export default ListUrls

Btw, I would really want to use hooks to do it :)

So, I would except to see this table, but the page is blank :/

enter image description here


Solution

  • As pointed out by the others, you will need to use useEffect in this case. Otherwise your fetchData will be called over and over every time you update the state. I would also change the way you call those URLs to get their http status to using promises (just cuz I like promises). Here is how I would write your code:

    import { useEffect, useState } from "react";
    import "./styles.css";
    import jsonData from "./urls.json";
    
    export default function App() {
      const [data, setData] = useState(jsonData);
    
      useEffect(() => {
        const promises = jsonData.map((url, i) => {
          return fetch(url.url).then((r) => ({ fetch: r, index: i }));
        });
    
        Promise.all(promises)
          .then((result) => {
            const new_data = result.map((d) => {
              jsonData[d.index].status = d.fetch.status;
              return jsonData[d.index];
            });
            setData(new_data);
          })
          .catch((e) => {
            console.log("Handle error here");
          });
      }, []);
    
      return (
        <div>
          <table>
            <thead>
              <tr>
                <th>Name</th>
                <th>Status</th>
              </tr>
            </thead>
            <tbody>
              {data.map((url, i) => (
                <tr key={i}>
                  <td>
                    <a href={url.url} target={"_blank"}>
                      {url.name}
                    </a>
                  </td>
                  <td>{url.status}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      );
    }
    

    Note that I am using the regular <table> instead of your prebuilt element <Table> as I don't have it. Here is a sandbox: https://codesandbox.io/s/confident-jang-yrxv3?file=/src/App.js:285-286


    fetch returns a promise, which, when resolved, yields the fetch response object. So if you only have one fetch to do you can call it like so:

    fetch(url).then(response => {//do stuff here with response object})
    

    But let's say you have multiple fetch commands as you do in your case. How do you resolve them with just one then. Well that's where Promise.all() comes in handy. The idea is to get all the promises from your fetch commands and then resolve them all in one shot. As such:

    const promises = jsonData.map((url, i) => {
          return fetch(url.url).then((r) => ({ fetch: r, index: i }));
        });
    

    promises will be an array of promises that all the fetch commands inside the map return. One caveat here is I also return the index of the specific URL so that later you can use it to see which URL corresponds to which HTTP code. And finally you resolve them all using:

        Promise.all(promises)
          .then((result) => {
                ...
                ...
    

    Simple right?