I'm working on a school assignment that has been driving me crazy. It's a little game where the user clicks on one of two hamster pictures to pick the cutests that will win.
I had to use React + Typescript for this.
The game is working just fine now, but I get two messages in the console that I have to fix before sending the assignment in, but I don't understand how, especially because I feel that typescript is making it so much harder.
One is: The 'sendRequest' function makes the dependencies of useEffect Hook (at line 23) change on every render. Move it inside the useEffect callback. Alternatively, wrap the definition of 'sendRequest' in its own useCallback() Hook react-hooks/exhaustive-deps
And the other is: Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
The second one appears only after I start a new game.
Both this errors occur in my "fighterCardOverlay" component. Which is this:
import { Hamster } from '../../models/Hamster';
import { useEffect, useState } from 'react';
interface CardGridProps {
fighter: string;
showInfo: boolean;
}
type Hamsters = Hamster;
const FighterCardOverlay = ({ fighter, showInfo }: CardGridProps) => {
const [updatedData, setUpdatedData] = useState<Hamsters | null>(null);
async function sendRequest(saveUpdatedData: any) {
const response = await fetch('/hamsters/'+ fighter);
const data = await response.json()
saveUpdatedData(data)
}
useEffect(() => {
sendRequest(setUpdatedData)
}, [showInfo, sendRequest])
return (
<div className="fighterCardOverlay" >
{updatedData ?
<div>
<li>Name: {updatedData.name}</li>
<li>Age: {updatedData.age}</li>
<li>Loves: {updatedData.loves}</li>
<li>Favorite Food: {updatedData.favFood}</li>
<li>Games: {updatedData.games}</li>
<li>Wins: {updatedData.wins}</li>
<li>Defeats: {updatedData.defeats}</li>
</div>
: 'Loading info...'
}
</div>
)
}
export default FighterCardOverlay
The assignment required that after the user has selected the winner, more informations pop up about each hamster that the user can read, hence the overaly. To get the correct info with the updated number of games and wins/defeats for each hamster, I had to fetch the data again from the server after the user has selected the winner. To do so, I added on UseEffect that this should run everytime the state that toggles the overlay changes (showInfo), and this is sent as a prop from the "game" component, where the overlay is rendered.
Here is that code:
import { useEffect, useState } from 'react';
import { Hamster } from '../../models/Hamster';
import FighterCard from './FighterCard'
import '../../styles/game.css'
import FighterCardOverlay from './FighterCardOverlay';
type Hamsters = Hamster;
const Game = () => {
const [fighters, setFighters] = useState<Hamsters[] | null >(null);
const [showInfo, setShowInfo] = useState(false);
useEffect(() => {
getFighters(setFighters)
}, [])
function handleShowInfo(){
if(showInfo === true ){
setShowInfo(false)
} else if(showInfo === false) {
setShowInfo(true)
}
}
function newGame() {
getFighters(setFighters)
handleShowInfo()
}
function updateWin(id: string, wins: number, games: number) {
const newWins = wins + 1;
const newGames = games + 1;
return fetch('/hamsters/' + id, {
method: 'PUT',
headers: { "Content-Type": "application/json" },
body: JSON.stringify({wins: newWins, games: newGames})
})
}
function updateDefeats(id:string, defeats: number, games: number) {
const newDefeats = defeats + 1;
const newGames = games + 1;
return fetch('/hamsters/' + id, {
method: 'PUT',
headers: { "Content-Type": "application/json" },
body: JSON.stringify({defeats: newDefeats, games: newGames})
})
}
function updateWinnersAndLosers(winnerId: string) {
if( fighters ) {
const winner = fighters.find(fighter => fighter.id === winnerId);
const loser = fighters.find(fighter => fighter.id !== winnerId);
if(winner){
updateWin(winner.id, winner.wins, winner.games);
}
if(loser){
updateDefeats(loser.id, loser.defeats, loser.games);
};
handleShowInfo();
} else {
console.log('fighters is undefined')
}
}
return (
<div className='game'>
<h2>Let the fight begin!</h2>
<p>Click on the cutest hamster</p>
<section className="fighters">
{fighters
? fighters.map(fighter => (
<div className="fighterCardContainer" key={fighter.id}>
<FighterCard fighter={fighter} updateWinnersAndLosers={updateWinnersAndLosers} />
{showInfo && <FighterCardOverlay fighter={fighter.id} showInfo={showInfo}/>}
</div>
))
: 'Loading fighters...'}
</section>
<button className="gameButton" onClick={newGame}>New Game</button>
</div>
)
};
async function getFighters(saveFighters: any) {
try {
const response = await fetch('/hamsters');
const data = await response.json()
const fighters = await data.sort(() => .5 - Math.random()).slice(0,2)
saveFighters(fighters)
} catch (error) {
saveFighters(null)
}
};
export default Game
As I said, the game works just fine, I get the correct info and it doesn't crash. But any help to fix the errors in the console would be greatly appriciated!
It can be solved like this:
useEffect(() => {
const controller = new AbortController(); // It solves Warning: Can't perform a React state update on an unmounted component.
const signal = controller.signal;
(async () => { // you also can move it to useCallback (don't forget pass signal
try {
const response = await fetch('/hamsters/' + fighter, { signal });
const data = await response.json();
setUpdatedData(data);
} catch (error) {
// TODO: handle error
}
})();
return () => {
controller.abort();
};
}, [showInfo, fighter]);