I'm having some issues while rendering a component in my project. The goal of this component is to fetch some data with a 3rd party API and then display it in a HTML table (MatchTable component).
Profile.js
// import statements
export const Profile = () =>
{
const navigate = useNavigate();
const { id } = useParams();
const [state, setState] = useState(
{ arrays :
{
gameArray : [],
categoryArray : [],
mapArray : [],
victoryArray : [],
killsArray : [],
deathsArray : [],
},
counters :
{
totalKills : 0,
totalDeaths : 0,
totalVictories : 0,
totalLosses : 0,
totalKD : 0,
totalWL : 0
},
pageCounter : 0}
);
useEffect(() => {
state.pageCounter++;
}, []);
useEffect(() => {
const fetchHaloData = async () =>
{
const url1 = 'https://cryptum.halodotapi.com/games/hmcc/stats/players/' ;
const url2 = url1 + id;
const url3 = `${url2}/recent-matches?page=` + state.pageCounter.toString(); // API provides 25 rows of data per call.
const response = await fetch(url3, {
"method": 'GET',
"headers": {
'Content-Type': 'application/json',
'Cryptum-API-Version': '2.3-alpha',
'Authorization': 'MY_SECRET_TOKEN'
}
});
if (!response.ok)
console.log("Response not okay!");
else
{
const res = await response.json();
let data = {
gameArray : [],
categoryArray : [],
mapArray : [],
victoryArray : [],
killsArray : [],
deathsArray : [],
};
let stats = {
totalKills : 0,
totalDeaths : 0,
totalVictories : 0,
totalLosses : 0,
totalKD : 0,
totalWL : 0
};
if (state.arrays.gameArray[0] !== undefined) // data and stats need to be set to state initially to preserve the already fetched rows and stats calculations.
{
data = state.arrays;
stats = state.counters;
}
for (let i=0; i< res.data.length; i++)
{
data.victoryArray.push(res.data[i].victory);
data.victoryArray[data.victoryArray.length-1] ? stats.totalVictories++ : stats.totalLosses++
data.killsArray.push(res.data[i].stats.kills);
stats.totalKills += data.killsArray[data.killsArray.length-1];
data.deathsArray.push(res.data[i].stats.deaths);
stats.totalDeaths += data.deathsArray[data.deathsArray.length-1];
data.mapArray.push(res.data[i].details.map.name);
data.categoryArray.push(res.data[i].details.category.name);
data.gameArray.push(res.data[i].details.engine.name);
}
stats.totalKD = (stats.totalKills / stats.totalDeaths);
stats.totalWL = (stats.totalVictories / stats.totalLosses);
setState((prevState) => { return {...prevState, arrays : data, counters : stats}});
}
}
fetchHaloData().catch((error) => { console.log("There was an error:" + error)})
}, [id, state.pageCounter]);
let more;
if (state.pageCounter > 0 && state.pageCounter < 4)
{
if (state.arrays.gameArray.length % 25 === 0) // if the stats are less then 25 rows, there's no need to fetch more.
more = <button id="More" onClick={ () => { setState((prevState) => { return {...prevState, pageCounter : prevState.pageCounter + 1}})}}>More...</button>;
else
more = null;
}
if (state.pageCounter >= 4)
more = null;
return (
<div>
<NavBar />
<ButtonSearch />
<h3 id="search_result_counters">Showing last { state.arrays.gameArray.length } matches for "{ id }". Total K/D: { state.counters.totalKD }. Total W/L: { state.counters.totalWL }</h3>
<MatchTable recentMatches={ state.arrays }/>
{ more }
<CustomFooter position="stay_sticky" />
</div>
);
}
Although the data is correctly fetched, I'm experiencing this issue:
The table should have 25 rows when rendered, instead it has 50 rows. Rows from 26 to 50 are the exact repetition of rows from 1 to 25. Honestly, with dev tools, it's hard to have a clue of what's happening render after render, I just see that state.pageCounter is set at zero initiallly, and then, after the component is mounted, the first useEffect gets triggered and increments it. So the second useEffect that has pageCounter in its dependencies gets triggered and executes the first fetch, thus mutating the state and passing that as props to the MatchTable component that correctly fills the HTML table. Then, after this, there is a cascade of renders, or at least I guess so, because the parser goes back to the first useEffect but does not increment the pageCounter, which stays the same. After this chain of renders, I get some output in the Debug Output section:
12 Warning: unstable_flushDiscreteUpdates: Cannot flush updates when React is already rendering.
at Profile (http://localhost:3000/static/js/bundle.js:1360:81)
at Routes (http://localhost:3000/static/js/bundle.js:36570:5)
at Router (http://localhost:3000/static/js/bundle.js:36503:15)
at BrowserRouter (http://localhost:3000/static/js/bundle.js:35979:5)
8 Warning: unstable_flushDiscreteUpdates: Cannot flush updates when React is already rendering.
at MatchTable (http://localhost:3000/static/js/bundle.js:1028:5)
at div
at Profile (http://localhost:3000/static/js/bundle.js:1360:81)
at Routes (http://localhost:3000/static/js/bundle.js:36570:5)
at Router (http://localhost:3000/static/js/bundle.js:36503:15)
at BrowserRouter (http://localhost:3000/static/js/bundle.js:35979:5)
12 Warning: unstable_flushDiscreteUpdates: Cannot flush updates when React is already rendering.
at Profile (http://localhost:3000/static/js/bundle.js:1360:81)
at Routes (http://localhost:3000/static/js/bundle.js:36570:5)
at Router (http://localhost:3000/static/js/bundle.js:36503:15)
at BrowserRouter (http://localhost:3000/static/js/bundle.js:35979:5)
8 Warning: unstable_flushDiscreteUpdates: Cannot flush updates when React is already rendering.
at MatchTable (http://localhost:3000/static/js/bundle.js:1028:5)
at div
at Profile (http://localhost:3000/static/js/bundle.js:1360:81)
at Routes (http://localhost:3000/static/js/bundle.js:36570:5)
at Router (http://localhost:3000/static/js/bundle.js:36503:15)
at BrowserRouter (http://localhost:3000/static/js/bundle.js:35979:5)
Now, since the Debug Output seems two times repeated as well as the table content, I'm guessing the useEffect with fetch() is getting executed two times with the same value of pageCounter.
If it helps, I also have some warnings to report:
Line 42:8: React Hook useEffect has a missing dependency: 'state.pageCounter'. Either include it or remove the dependency array
react-hooks/exhaustive-deps
Line 188:8: React Hook useEffect has a missing dependency: 'state'. Either include it or remove the dependency array. You can also do a functional update 'setState(s => ...)' if you only need 'state' in the 'setState' call react-hooks/exhaustive-deps
This is unclear to me aswell. The first warning refers to the first useEffect hook, but the inclusion of state.pageCounter in the dependency array is not there because I want it to trigger only and only when the component renders the first time, hence the empty dependency array. The second warning refers to the second useEffect hook, and I did not include "state" generically because I called setState in the hook, otherwise I would create an infinite loop.
The logic of my dependency arrays is based on the algorithm: pageCounter is used to call the API, since it loads only 25 rows of data. At the beginning, I want to display only the first 25 rows of recent matches, with a "more" button that permits to fetch other 25 rows of data (up to 100 rows totally) by incrementing the pageCounter, which is stored into the state and his change would trigger a re-render with new parameters. In order to do this, I thought of triggering the fetch() only when the id of the player search changes, and only when the pageCounter changes (since I have to call the API that provides the next rows of data). The first useEffect is there to trigger the second useEffect for the very first time (when Profile gets mounted, actually). Since it listens to pageCounter changes, I trigger it by initializing the counter to zero and manually incrementing it after the very first render.
The problem I explained does not happen when I click on the "more" button. At the very first render, I would have 50 rows instead of 25, with the first 25 repeated in order. Then I click the button, and the rows become 75 with 25 new rows of new data (which is correct).