You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js14井字棋应用触发Received NaN for `children`属性警告求助

问题

使用Next.js 14开发井字棋(Tres en Raya)Web应用时,点击“Ver Ranking”按钮进入排名页面,触发如下警告:

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)

主页(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值。

修复步骤

  1. 修复总场次计算,避免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) }
    
  2. 为单个统计值添加默认值
    其他列的统计值同样处理未加载或缺失情况,添加默认值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部分统计值做相同修改。

  3. 优化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 }
        };
      }
    };
    
  4. 初始化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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 07:49:52