简体   繁体   中英

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:/

在此处输入图像描述

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?

You should change jsonArray to jsonData as the parameter and set the initial state to be null.

Currently you are passing in the state (jsonArray) instead of jsonData when calling the function. Also you should put the call to fetchData in a useEffect with jsonData as the parameter:

useEffect(() => fetchData(jsonData), [jsonData, fetchData]);

Would this become a loop? Component loads -> fetchData() is called -> fetchData() updates the state -> Component reloads -> repeat.

You could use a useEffect() hook with an empty array as the second argument to only run once:

useEffect(() => {
  //Update json as you were doing;
}, []);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM