I am building a tic tac toe web app. When I try it and click the button “Ver Ranking”, it throws me a warning I do not know how to handle:
Warning: Received NaN for the `children` attribute. If this is expected, cast the value to a string.
at td
at tr
at tbody
at table
at div
at div
at Component /./src/app/ranking/page.js:20:82)
I am building the app using Next.js 14 and this is my page.js code:
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Button from "@/components/Button";
import { getRanking, updateRanking } from "@/lib/apiCalls";
const winCombs = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
export default function Home() {
const [userTurn, setUserTurn] = useState(true);
const [boardState, setBoardState] = useState({
0: "",
1: "",
2: "",
3: "",
4: "",
5: "",
6: "",
7: "",
8: "",
});
const [isWinner, setIsWinner] = useState(false);
const [draw, setDraw] = useState(false);
const [title, setTitle] = useState("");
const [winnerCombo, setWinnerCombo] = useState([]);
const router = useRouter();
useEffect(() => {
if (!userTurn) {
setTimeout(() => {
aiPlay(boardState);
}, 1000);
}
checkResult(boardState);
}, [boardState, userTurn]);
const aiPlay = async (board) => {
try {
const res = await fetch(API_URL, {
method: "POST",
body: JSON.stringify(board),
headers: {
"content-type": "application/json",
},
});
const data = await res.json();
setBoardState(data);
setUserTurn(true);
} catch (error) {
console.log(error);
}
};
const updateBoard = (idx) => {
if (!boardState[idx] && !isWinner) {
let value = userTurn === true ? "X" : "O";
setBoardState({ ...boardState, [idx]: value });
setUserTurn(!userTurn);
}
};
const checkResult = async (board) => {
// board is filled
const allFilled = Object.values(board).every((cell) => cell !== "");
// state of the ranking at the moment
const actualRanking = await getRanking();
if (allFilled && !isWinner) {
console.log(userTurn);
setIsWinner(true);
setDraw(true);
setTitle("EMPATE!");
await updateRanking("-", actualRanking);
return;
}
for (let comb of winCombs) {
const [a, b, c] = comb;
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
if (!userTurn) {
console.log(userTurn);
setIsWinner(true);
setWinnerCombo([a, b, c]);
setTitle("VICTORIA!");
await updateRanking("X", actualRanking);
return;
}
if (userTurn) {
console.log(userTurn);
setIsWinner(true);
setWinnerCombo([a, b, c]);
setTitle("DERROTA!");
await updateRanking("O", actualRanking);
return;
}
}
}
};
const reset = () => {
setBoardState({
0: "",
1: "",
2: "",
3: "",
4: "",
5: "",
6: "",
7: "",
8: "",
});
setUserTurn(true);
setIsWinner(false);
setDraw(false);
setWinnerCombo([]);
setTitle("");
};
return (
<div>
<div className="flex flex-col items-center mt-1 mb-4 font-bold">
<h1>Tres en Raya</h1>
</div>
<div className="flex flex-col items-center">
<div className="text-center text-2xl mb-2">
<p>{userTurn === true ? "Es tu turno!" : "Turno de la IA"}</p>
<p
className={`text-sm mb-2 ${
!isWinner && !draw ? "text-green-600" : "text-red-600"
}`}
>{`${
isWinner || draw ? "Partida finalizada" : "Juego en progreso"
}`}</p>
</div>
<div className="grid grid-cols-[repeat(3,1fr)] gap-2">
{[...Array(9)].map((v, idx) => {
return (
<div
key={idx}
className={`bg-[#eee] ${
userTurn ? "shadow-[0px_4px_#ddd]" : "shadow-none"
} text-center text-[64px] leading-[100px] font-[bold] w-[100px] h-[100px] cursor-pointer rounded-lg ${
winnerCombo.includes(idx)
? "bg-black text-white shadow-none"
: ""
}`}
onClick={() => {
updateBoard(idx);
}}
>
{boardState[idx]}
</div>
);
})}
</div>
</div>
{isWinner || draw ? (
<div className="flex flex-col items-center mt-6">
<p className="text-xl font-bold mb-4">{title}</p>
<Button text="Volver a Jugar" action={reset} />
</div>
) : null}
<Button
text="Ver Ranking"
action={() => {
router.push("/ranking");
}}
/>
</div>
);
}
And I created a ranking page which code is:
"use client";
import Button from "@/components/Button";
import { useState, useEffect } from "react";
import { getRanking } from "@/lib/apiCalls";
import { useRouter } from "next/navigation";
export default function Component() {
const [ranking, setRanking] = useState({});
const router = useRouter();
useEffect(() => {
getRankingData();
}, []);
const getRankingData = async () => {
const rankingData = await getRanking();
setRanking(rankingData);
};
return (
<div className="container mx-auto px-4 md:px-6 py-8">
<div className="flex flex-col md:flex-row items-center justify-between mb-6">
<h1 className="font-bold mb-4 md:mb-0">Ranking</h1>
</div>
<div className="border rounded-lg overflow-hidden">
<table className="w-full table-auto">
<thead>
<tr>
<th className="w-[200px] px-4 py-3 text-left font-bold">
Jugador
</th>
<th className="px-4 py-3 text-right font-bold">Victorias</th>
<th className="px-4 py-3 text-right font-bold">Derrotas</th>
<th className="px-4 py-3 text-right font-bold">Empates</th>
<th className="px-4 py-3 text-right font-bold">PJs</th>
</tr>
</thead>
<tbody>
<tr>
<td className="px-4 py-3 font-medium">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-800 flex items-center justify-center">
<span>U</span>
</div>
<span>Usuario</span>
</div>
</td>
<td className="px-4 py-3 text-right">{ranking?.playerX?.won}</td>
<td className="px-4 py-3 text-right">{ranking?.playerX?.lost}</td>
<td className="px-4 py-3 text-right">{ranking?.playerX?.tied}</td>
<td className="px-4 py-3 text-right">
{ranking?.playerX?.won +
ranking?.playerX?.lost +
ranking?.playerX?.tied}
</td>
</tr>
<tr>
<td className="px-4 py-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-800 flex items-center justify-center">
<span>IA</span>
</div>
<span>IA</span>
</div>
</td>
<td className="px-4 py-3 text-right">{ranking?.playerO?.won}</td>
<td className="px-4 py-3 text-right">{ranking?.playerO?.lost}</td>
<td className="px-4 py-3 text-right">{ranking?.playerO?.tied}</td>
<td className="px-4 py-3 text-right">
{ranking?.playerO?.won +
ranking?.playerO?.lost +
ranking?.playerO?.tied}
</td>
</tr>
</tbody>
</table>
</div>
<Button
text="Volver a Jugar"
action={(e) => {
e.preventDefault();
router.push("/");
}}
/>
</div>
);
}
In this last page I get the data from a database and show it in a ranking table. Here it is the getRanking method:
export const getRanking = async () => {
try {
const res = await fetch(API_URL, {
method: "GET",
headers: {
"content-type": "application/json",
},
});
const data = await res.json();
if (data) return data[0];
return;
} catch (error) {
console.log(error);
}
};
I tried to modify the Button component but it did not solve the problem. Could be useEffect() in the ranking page the key point?
If you need further info I can provide it. Thanks in advance!