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:
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:
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.useFetch
should abort the fetches that are in progress if it's about to get a new set ofurls
, 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.