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

Time:12-02

I have an array of data resembling the following:

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

I also have code resembling the following:

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

which is called in

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

My issue is, when I run the application, I only get one output from my data (when I console.log(data) it returns the data I showed above), either A: 1 OR B:2. I expect there to be both present in the component. Any advice?

---- Update ---- useFetch function

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;

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

--- Update Page ---

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

---- Update again with the suggested fetch, unfortunately still overwriting ---

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;

Stack Snippet:

Show code snippet

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>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

CodePudding user response:

Your Page component creates a new componentsPage object with new urls arrays in the components every time it renders. Those new urls arrays are ultimately passed to useFetch (aka useFetchHook), where you have this structure:

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

That means that every time the urls parameter changes value (the old value isn't === the new value), it will repeat the fetch and update data or error.

There are various issues with the hook as well, the primary problem being that it does asynchronous work (a series of fetch calls) but doesn't check to be sure that the results its getting aren't outdated (because urls changed). More on that in a moment.

Since the urls arrays are recreated every time, useFetch does the fetches again every time, because no array is ever === any other array, even if they have the same contents:

console.log(["1", "2", "3"] === ["1", "2", "3"]); // false
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

So you need to:

  1. Have useFetch only start a new series of fetches when the URLs really change. If it's given a new array with the same contents, it shouldn't do a new set of fetches.

  2. useFetch should abort the fetches that are in progress if it's about to get a new set of urls, and shouldn't use the previous results if that's happened.

You seem to have started on #2 by using an AbortController, but nothing every called its abort method, so it didn't do anything.

Here's a version of useFetch that handles both of those things, see the comments:

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

That's a sketch, I won't be surprised if you need to make small tweaks to it, but it should get you going the right way.

  • Related