【问题标题】:Reactjs overwriting component when I try to create a component per item in my list当我尝试为列表中的每个项目创建一个组件时,Reactjs 覆盖组件
【发布时间】:2022-01-08 06:56:07
【问题描述】:

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

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) 它返回我上面显示的数据时),要么 答: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>
  );
}

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

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>

【问题讨论】:

  • 我预计您会遇到语法错误,{data !== null &lt;ReportComponent data={data} /&gt;}null 之后缺少&amp;&amp;。此外,您使用&lt;Box&gt; 打开但使用&lt;/Grid&gt; 关闭。请使用 minimal reproducible example 来更新您的问题,以展示问题,最好是使用 Stack Snippets([&lt;&gt;] 工具栏按钮)的 runnable 问题。 Stack Snippets 支持 React,包括 JSX; here's how to do one.
  • 我更新了它 - 我将尝试制作一个 Stack Snippets,但上面的代码几乎就是我所拥有的(除非在页面中调用 ReportComponent 并呈现)
  • 代码对我来说看起来不错(除了您需要在Typography 返回的Typography 元素上使用key),所以我将它复制并粘贴到一个sn-p 中并写了为TypographyBoxuseFetchHooik 设置一些占位符/替身。它工作得很好。请更新 sn-p 以演示问题(并更新 sn-p 之外的问题中的代码以匹配,或者只是将其删除并取消选中“默认隐藏 sn-p”框)。
  • 所以它会短暂显示一切正常,然后它会切换回只有一个项目。我知道数据在我将其记录到控制台时按预期存在,但它会在交换为组件中只有一项后立即出现。我认为问题出在我获取数据时,我更新了我将使用 useState 返回的值。但是,在我的 while 循环完成之前,我不会返回 { data }。我不确定为什么会发生此错误。任何建议将不胜感激!
  • 现在添加断点。顺便说一句,非常感谢!

标签: javascript reactjs components


【解决方案1】:

您的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 版本,请参阅 cmets:

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 };
}

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

【讨论】:

  • 我通常有一个坚定的“不要发布和运行”政策,但现在是晚餐时间,我被告知它在桌子上,所以...:-)
  • 我非常欣赏这一点。不用担心晚餐,这会给我一些时间看看。祝您晚餐愉快,再次感谢您!
  • 问题 - urlsRef.current 是什么?我不熟悉这个概念。希望晚餐很可爱!
  • @ChristinaStebbins - 很高兴为您提供帮助。 ursRef.currentref 的当前值。更多关于钩子hre.
  • 嗨,T.J.!我实施了您的建议 - 它似乎没有解决我的两个数据粒子未显示的问题。我正在上传我的实现。希望可以通过更多的工作来解决问题! (您的实现有效,我似乎仍然在页面的单个渲染中获取数据两次)。也许问题是我将数据附加到结果 const 以使用所有包含在一个位置的数据。如果您有任何想法,请告诉我,但感谢您的帮助!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多