当我尝试为列表中的每个项目创建一个组件时,Reactjs 覆盖组件

Reactjs overwriting component when I try to create a component per item in my list

我有一组类似于以下内容的数据:

data = [{name: 'A', data: 1}, {name: 'B', data: 2}]

我也有类似于以下的代码:

function ReportComponent({ data }) {
  return data.map((datum) => (
    <Typography>
      {datum.name}: {datum.data}
    </Typography>
  ));
}

中调用
function ReportBox({ component }) {
  const { data } = useFetchHook(component.urls)
  // data returns exactly as expected, an array of objects
  return (
    <Box>
      <Typography>
        {component.title}
      </Typography>
      {data !== null && <ReportComponent data={data} />}
    </Box>
  );
}

我的问题是,当我 运行 应用程序时,我只从我的数据中得到一个输出(当我 console.log(data) 它 returns 我上面显示的数据时) , 任何一个 答:1 或 B:2。我希望组件中同时存在。有什么建议吗?

----更新---- 使用Fetch函数

import { useState, useEffect } from 'react';

function useFetch(urls) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    let i = urls.length - 1;
    const result = [];

    while (i >= 0) {
      const abortCont = new AbortController();
      console.log(`url ${i}`);
      console.log(urls[i]);
      fetch(urls[i], { signal: abortCont.signal }, { mode: 'cors' })
        .then((res) => {
          if (!res.ok) {
            console.log('something went wrong with the data fetch');
          }
          return res.json(); // why?
        })
        .then((data) => {
          result.push(data);
          setData(result);
        })
        .catch((err) => {
          if (err.name === 'AbortError') {
            console.log('aborted');
          } else {
            setError(err.message);
          }
        });
      i -= 1;
    }
  }, [urls]);
  // console.log(data);

  return { data, error };
}

export default useFetch;

--- 更新 DashBox ---

mport { Box, Grid, Container, Typography } from '@mui/material';
import ReportBox from './ReportBox';

function DashBox({ components }) {
  // console.log(components);
  return (
    <Grid
      item
      columns={5}
      sx={{
        display: 'flex',
        flexDirection: 'row',
        justifyContent: 'space-evenly',
        alignItems: 'stretch',
        marginTop: '20px',
        marginLeft: '5px'
      }}
    >
      {components.map((component) => (
        <ReportBox component={component} />
      ))}
    </Grid>
  );
}

export default DashBox;

---更新页面---

export default function Page() {
  const optionsFilter= [
    'A',
    'B',
    'C'
  ];
  const [filter, setFilter] = useState('A');

  const componentsPage = [
    {
      title: 'One',
      urls: [
        `http://localhost:9000/page1?filter=${filter}`,
        `http://localhost:9000/page2?filter=${filter}`
      ]
    }
  ];

  const componentsPageGraphs = [
    {
      title: 'OneGraph',
      urls: [
        `http://localhost:9000/page1?filter=${filter}`,
        `http://localhost:9000/page2?filter=${filter}`
      ]
    }
  ];

  return (
    <Page title="Page">
      <Container>
        <Typography variant="h4" sx={{ mb: 5 }}>
          Page
        </Typography>
        <Container marginBottom="10px">
          <Typography marginLeft="5px" variant="h5">
            Filters
          </Typography>
          <Grid
            columns={5}
            sx={{
              display: 'flex',
              flexDirection: 'row',
              alignItems: 'stretch',
              marginTop: '10px',
              marginLeft: '5px',
              justifyContent: 'space-evenly'
            }}
          >
            <Grid item sx={{ pr: 5 }}>
              <DropDown
                options={optionsFilter}
                title="Filter Type"
                setData={setFilter}
                data={filter}
                key="one"
              />
            </Grid>
          </Grid>
        </Container>
        <br />
        <Box
          container
          sx={{ border: 2 }}
          marginLeft="20px"
          pr="20px"
          pb="20px"
          pl="20px"
          width="100%"
        >
          <Typography variant="h3">Page Dashboard</Typography>
          <DashBox components={componentsPage} />
        </Box>
        <Grid container spacing={2} marginTop="20px">
          {componentsPageGraphs.map((component) => (
            <Grid item xs={6}>
              <Typography>{component.title}</Typography>
              <LineChart xtype="category" urls={component.urls} />
            </Grid>
          ))}
        </Grid>
      </Container>
    </Page>
  );
}

---- 使用建议的获取再次更新,不幸的是仍然覆盖 ---

import { useState, useEffect, useRef } from 'react';

const sameContents = (array1, array2) =>
  array1.length === array2.length && array1.every((value, index) => value === array2[index]);

function useFetch(urls) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const urlsRef = useRef(null);

  if (!urlsRef.current || !sameContents(urlsRef.current, urls)) {
    urlsRef.current = urls.slice();
  }

  useEffect(() => {
    const results = [];
    if (!urlsRef.current) {
      return;
    }
    const controller = new AbortController();
    const { signal } = controller;

    Promise.all(
      urlsRef.current.map((url) => {
        fetch(url, { signal, mode: 'cors' })
          .then((res) => {
            if (!res.ok) {
              console.log('http issue');
            }
            return res.json();
          })
          .then((data) => {
            if (!signal.aborted) {
              results.push(data);
              setData(results);
              setError(null);
            }
          })
          .catch((error) => {
            if (signal.aborted) {
              return;
            }
            setData(null);
            setError(error);
          });
        return () => {
          controller.abort();
        };
      })
    );
  }, [urlsRef.current]);
  return { data, error };
}

export default useFetch;

堆栈片段:

const {useState, useEffect} = React;

// Fake Typography component
const Typography = ({children}) => <div>{children}</div>;

// Fake Box component
const Box = ({children}) => <div>{children}</div>;

// Fake fetch hook
function useFetchHook(urls) {
    const [data, setData] = useState(null);
    useEffect(() => {
        setTimeout(() => {
            setData([
                {name: "One", data: "Data for 'One'"},
                {name: "Two", data: "Data for 'Two'"},
                {name: "Three", data: "Data for 'Three'"},
            ]);
        }, 500);
    }, []);
    return {data};
}

function ReportComponent({ data }) {
    return data.map((datum) => (
        <Typography>
            {datum.name}: {datum.data}
        </Typography>
    ));
}

function ReportBox({ component }) {
    const { data } = useFetchHook(component.urls)

    // data returns exactly as expected, an array of objects
    return (
        <Box>
            <Typography>
                {component.title}
            </Typography>
            {data !== null && <ReportComponent data={data} />}
        </Box>
    );
}

ReactDOM.render(<ReportBox component={{urls: [], title: "Example"}} />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

您的 Page 组件创建一个 new componentsPage 对象,其中包含 new urls 数组组件 每次 渲染。那些新的 urls 数组最终会传递给 useFetch(又名 useFetchHook),在那里你有这样的结构:

function useFetch(urls) {
    const [data, setData] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        // ...code that fetches and sets `data`/`error`...
    }, [urls]);
    // console.log(data);

    return { data, error };
}

这意味着每次urls参数改变值(旧值不是===新值),它将重复获取和更新 dataerror.

挂钩也存在各种问题,主要问题是它执行异步工作(一系列 fetch 调用)但不检查以确保其获得的结果是'已过时(因为 urls 已更改)。稍后会详细介绍。

由于每次都会重新创建 urls 数组,因此 useFetch 每次都会重新获取,因为没有数组会 === 任何其他数组,即使它们具有相同的数组内容:

console.log(["1", "2", "3"] === ["1", "2", "3"]); // false

所以你需要:

  1. useFetch 仅在 URL 真正改变时才开始一系列新的提取。如果给它一个具有相同内容的新数组,它不应该执行一组新的提取操作。

  2. useFetch 如果要获取一组新的 urls,则应中止正在进行的提取,如果发生这种情况,则不应使用以前的结果。

您似乎是通过使用 AbortController 在 #2 上开始的,但是没有任何东西调用它的 abort 方法,所以它什么也没做。

这是处理这两种情况的 useFetch 版本,请参阅评论:

const sameContents = (array1, array2) => {
    return array1.length === array2.length &&
           array1.every((value, index) => value === array2[index]);
};

function useFetch(urls) {
    const [data, setData] = useState(null);
    const [error, setError] = useState(null);
    const urlsRef = useRef(null);               // A place to keep the URLs we're handling

    if (!urlsRef.current ||                     // Mounting, or
        !sameContents(urlsRef.current, urls)    // Called after mount with *different* URLs
       ) {
        // Remember these URLs
        urlsRef.current = urls.slice();
    }

    useEffect(() => {
        if (!urlsRef.current) {
            // Nothing to do
            return;
        }
        // Use the same controller and signal for all the fetches
        const controller = new AbortController();
        const {signal} = controller;
        // Use `Promise.all` to wait for all the fetches to complete (or one
        // of them to fail) before setting `data`.
        Promise.all(urlsRef.current.map(url =>
            // Note: You had `{ mode: "cors" }` on its own as a third argument,
            // but it should have been part of the second argument (`fetch`
            // only takes two).
            fetch(url, {signal, mode: "cors"})
            .then(res => {
                if (!res.ok) {
                    // HTTP error
                    throw new Error(`HTTP error ${res.status}`);
                }
                // HTTP okay, read the body of the response and parse it
                return res.json();
            })
        ))
        .then(data => {
            // Got all the data. If this set of results isn't out of date,
            // set it and clear any previous error
            if (!signal.aborted) {
                setData(data);
                setError(null);
            }
        })
        .catch(error => {
            // Do nothing if these results are out of date
            if (signal.aborted) {
                return;
            }
            // Clear data, set error
            setData(null);
            setError(error);
        });
        // Return a cleanup callback to abort the set of fetches when we get
        // new URLs.
        return () => {
            controller.abort();
        };
    }, [urlsRef.current]); // <=== Use this instead of `urls`

    return { data, error };
}

这是一个 草图,如果您需要对其进行一些小的调整,我不会感到惊讶,但它应该会让您走上正确的道路。