Next.js14井字棋应用触发Received NaN for `children`属性警告求助
使用Next.js 14开发井字棋(Tres en Raya)Web应用时,点击“Ver Ranking”按钮进入排名页面,触发如下警告:
Warning: Received NaN for the
childrenattribute. 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)
主页(page.js)代码
"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> ); }
排名页面(page.js)代码
"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> ); }
getRanking方法代码
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); } };
问题原因
警告核心是排名页面计算总场次(PJs列)时,在ranking数据未加载或缺失的情况下,undefined值相加得到NaN,而React不允许将NaN作为DOM子节点渲染。此外,getRanking方法在无数据或请求失败时返回undefined,导致ranking状态异常,进一步引发更多undefined值。
修复步骤
修复总场次计算,避免NaN
修改排名页面PJs列代码,为每个统计值添加默认值0,确保相加结果为有效数字:// 用户总场次 { (ranking?.playerX?.won || 0) + (ranking?.playerX?.lost || 0) + (ranking?.playerX?.tied || 0) } // AI总场次 { (ranking?.playerO?.won || 0) + (ranking?.playerO?.lost || 0) + (ranking?.playerO?.tied || 0) }为单个统计值添加默认值
其他列的统计值同样处理未加载或缺失情况,添加默认值0:<td className="px-4 py-3 text-right">{ranking?.playerX?.won || 0}</td> <td className="px-4 py-3 text-right">{ranking?.playerX?.lost || 0}</td> <td className="px-4 py-3 text-right">{ranking?.playerX?.tied || 0}</td>AI部分统计值做相同修改。
优化getRanking方法返回值
修改getRanking方法,确保无数据或请求失败时返回默认结构对象,避免ranking状态变为undefined: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 && data[0]) { return data[0]; } // 返回默认结构 return { playerX: { won: 0, lost: 0, tied: 0 }, playerO: { won: 0, lost: 0, tied: 0 } }; } catch (error) { console.log(error); // 异常时返回默认结构 return { playerX: { won: 0, lost: 0, tied: 0 }, playerO: { won: 0, lost: 0, tied: 0 } }; } };初始化ranking状态为默认结构
排名页面中初始化ranking状态为默认结构,避免初始渲染时出现undefined:const [ranking, setRanking] = useState({ playerX: { won: 0, lost: 0, tied: 0 }, playerO: { won: 0, lost: 0, tied: 0 } });
额外优化
可添加加载状态提升用户体验:
const [loading, setLoading] = useState(true); const getRankingData = async () => { setLoading(true); const rankingData = await getRanking(); setRanking(rankingData); setLoading(false); }; // 在组件返回中添加加载提示 if (loading) { return <div className="container mx-auto px-4 py-8">Cargando ranking...</div>; }
内容的提问来源于stack exchange,提问作者Martín Rodas

