I would like to know how to correctly load data before rendering in React.
I have app, that fetchs data about Star Wars character from API and renders it on the page. I want also to fetch next character data before i want to render it by clicking on the button. I have an example of this app:
`
function fetchData(count, setState) {
fetch("https://swapi.dev/api/people/" count)
.then(res => res.json())
.then(data => setState({
id: count,
name: data.name,
birth_year: data.birth_year,
gender: data.gender
}))
}
function App() {
let [data, setData] = React.useState({});
let [preloadedData, setPreloadedData] = React.useState({});
let [count, setCount] = React.useState(1);
React.useEffect(() => {
if (count === 1) {
fetchData(count, setData)
fetchData(count 1, setPreloadedData);
}
else {
setData(preloadedData)
fetchData(count 1, setPreloadedData);
}
}, [count]);
console.log(count)
return (
<main>
<h1>Starwars character data</h1>
<p><span>Number:</span> {data.id}</p>
<p><span>Name:</span> {data.name}</p>
<p><span>Birth year:</span> {data.birth_year}</p>
<p><span>Gender:</span> {data.gender}</p>
<button onClick={() => setCount(count 1)}>Next character</button>
</main>
)
}
`
I have 2 questions: 1)is this a correct way of solving my problem, or there can be better practices? 2)I guess this way still could be better, because console.log is called 3 times, so my component renders 3 times each time the count state is changed, and it could be less?
Would like to know your opinion
CodePudding user response:
if you don't want that your useEffect execute 3 times you can pass an empty array, this way it will only execute when mounting and unmounting yor component, like this:
React.useEffect(() => {
if (count === 1) {
fetchData(count, setData)
fetchData(count 1, setPreloadedData);
}
else {
setData(preloadedData)
fetchData(count 1, setPreloadedData);
}
}, []); //Look here ;)
And yes fetching stuff in the useEffect its good but, for complex apps it is much better to use Redux and fetching the data with a Thunk. Another good practice is cleanning stuff by returning a callback function at the end of your useEffect, like this:
React.useEffect(() => {
if (count === 1) {
fetchData(count, setData)
fetchData(count 1, setPreloadedData);
}
else {
setData(preloadedData)
fetchData(count 1, setPreloadedData);
}
return () => {
//Clean things here
}
}, []);
I don't know if it anwers your question but I hope it helps
CodePudding user response:
1)is this a correct way of solving my problem
It seems to work, which is already an acceptable solution.
For sure it has some limitations, but depending on your actual requirements, you may bare with them. In particular, if user clicks quickly several times on the "Next character" button (before next data is received), you may de-sync count
and data
, and if the several created fetch requests give unordered responses, preloadedData
may also not reflect count 1
.
2)I guess this way still could be better, because console.log is called 3 times, so my component renders 3 times each time the count state is changed, and it could be less?
You do not necessarily have to worry about re-rendering (in the sense that the component function is re-executed): React updates its Virtual DOM, but if it sees no change in the output, it does not touch the real DOM (hence almost no UI impact).
In your case:
count
is incremented, but your template is unaffected => no DOM changedata
is changed, which affects the template values => DOM is modifiedpreloadedData
is updated, not affecting the template => no DOM change
there can be better practices?
We can always improve our code!
In your case, you could address the above mentioned limitations, and at the same time potentially provide the extra feature of enabling "previous character" navigation, while still re-using preloaded data.
For that, we would need to accumulate preloaded data with their associated id
(count
), e.g. with react-use useList
hook:
const [count, setCount] = React.useState(1);
const [list, { updateAt }] = useList([]);
const data = list[count - 1] ?? {}; // index/position is 0-based, whereas count starts at 1, so position is offset by -1 compared to count
function addItem(atPosition) {
updateAt(atPosition, {}); // Empty data for now
fetchData(
atPosition 1,
// Store received data at the correct position
(preloadedData) => updateAt(atPosition, preloadedData)
);
}
// Initially fill the first 2 items
React.useEffect(() => {
addItem(0);
addItem(1);
}, []);
function handleClickNext() {
const nextCount = count 1;
setCount(nextCount);
// Ensure list always has at least 1 more item than nextCount
for (let atPosition = list.length; atPosition <= nextCount; atPosition = 1) {
addItem(atPosition);
}
}
function handleClickPrevious() {
if (count > 1) { // Prevent navigating to count <= 0
setCount(count - 1);
}
// No need to update list, necause it should already have been populated when navigating up
}
return (
<main>
{/* data display... */}
<button onClick={handleClickPrevious} disabled={count <= 1}>Previous character</button>
<button onClick={handleClickNext}>Next character</button>
</main>
);