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

React子函数组件useEffect无法更新Context,仅存最后Tile坐标

问题:棋盘Tile坐标无法全部存入Context

开发场景

开发带方格的游戏棋盘,单位初始位于棋盘外,游戏启动后用户可将单位拖拽至指定Tile上。需在App Context中跟踪所有Tile的X、Y坐标,以此判断单位落于哪个Tile。

预期效果

Tile组件挂载时,通过useEffect将自身位置对象添加到Context的tilesCoords数组中。

问题现象

实际Context中始终仅保留最后一个Tile的坐标。

已尝试操作

  • 使用useRef包裹Context无改善;
  • 为useEffect添加依赖会引发无限循环,且仍仅保留最后一个Tile坐标。

代码实现

App.js

import { Encampment } from "./components/Encampment";
import "./App.css";
import { Battlefield } from "./components/Battlefield";
import { Unit } from "./components/Unit";
import { ARMY } from "./constants";
import { useState, createContext } from "react";

export const CoordsContext = createContext<{
  tilesCoords: object[];
  setTilesCoords: React.Dispatch<React.SetStateAction<object[]>>;
}>({
  tilesCoords: [],
  setTilesCoords: () => {},
});

function App() {
  const [tilesCoords, setTilesCoords] = useState<object[]>([]);

  const armyColours = ["black", "white"];

  let whiteArmy: React.ReactElement[] = [];
  let balckArmy: React.ReactElement[] = [];

  armyColours.forEach((colour) => {
    Object.entries(ARMY).forEach((entries) => {
      const [ObjKey, value] = entries;
      for (let index = 0; index < value; index++) {
        const name = `${ObjKey}-${colour}`;
        const key = `${name}_${index}`;
        colour === "white"
          ? whiteArmy.push(<Unit name={name} key={key} />)
          : balckArmy.push(<Unit name={name} key={key} />);
      }
    });
  });

  return (
    <div className="App">
      <CoordsContext.Provider value={{ tilesCoords, setTilesCoords }}>
        <Encampment units={whiteArmy} />
        <Battlefield />
        <Encampment units={balckArmy} />
      </CoordsContext.Provider>
    </div>
  );
}

Tile.js

import { FC, useRef } from "react";
import "./styles/Tile.css";
import { TileProps } from "../types/TileProps";
import { CoordsContext } from "../App";
import React, { useContext, useEffect } from "react";

const BLUE = "#4364db";
const GREEN = "#6cbc6c";
const YELLOW = "#b8b877";
const BROWN = "#8d5a5a";
const BLACK = "black";

const Tile: FC<TileProps> = ({ terrain, isHill, children }) => {
  const tileRef = useRef<HTMLDivElement>(null);

  const getPosition = () => {
    if (tileRef.current) {
      const x = tileRef.current.offsetLeft;
      const y = tileRef.current.offsetTop;
      return [x, y];
    }
  };
  const context = useRef(useContext(CoordsContext));
  const { tilesCoords, setTilesCoords } = context.current;

  useEffect(() => {
    const position = getPosition();
    console.log('position', position)
    console.log('children', children)
    console.log("tilesCoords", tilesCoords);
    setTilesCoords([...tilesCoords, { position, key: children }]);
    // need to pass dependencies but in a way that doesn't cause infinite loop
  }, []);

  let backgroundColor;
  switch (terrain) {
    case "water":
      backgroundColor = BLUE;
      break;
    case "grass":
      backgroundColor = GREEN;
      break;
    case "sand":
      backgroundColor = YELLOW;
      break;
    case "dirt":
      backgroundColor = BROWN;
      break;
    case "mount":
      backgroundColor = BLACK;
      break;
  }

  const outline = isHill ? "2px solid black" : "none";
  const zIndex = isHill ? 2 : 0;

  const tileStyle = {
    backgroundColor,
    outline,
    zIndex,
  };

  return <div className="tile" style={tileStyle} ref={tileRef}></div>;
};

export default Tile;

问题分析与解决方案

问题根源

  1. 闭包捕获旧值:useEffect依赖为空数组,导致内部的tilesCoords始终是组件挂载时的初始空数组。每个Tile执行setTilesCoords([...tilesCoords, newItem])时,都是基于空数组添加元素,最终只会保留最后一个Tile的坐标。
  2. 不必要的useRef包裹Context:用useRef包裹useContext会捕获组件挂载时的旧Context值,无法获取最新的setTilesCoords和tilesCoords。

修复方案

修改Tile组件中的Context获取和useEffect逻辑:

  • 移除包裹Context的useRef,直接通过useContext获取最新的Context值;
  • 使用函数式更新调用setTilesCoords,确保基于最新的数组状态添加新元素,无需依赖tilesCoords;
  • 保持useEffect依赖为[setTilesCoords],它是React稳定的函数引用,不会引发无限循环。

修改后的Tile.js关键代码

const Tile: FC<TileProps> = ({ terrain, isHill, children }) => {
  const tileRef = useRef<HTMLDivElement>(null);

  const getPosition = () => {
    if (tileRef.current) {
      const x = tileRef.current.offsetLeft;
      const y = tileRef.current.offsetTop;
      return [x, y];
    }
  };
  // 直接获取Context,无需useRef包裹
  const { setTilesCoords } = useContext(CoordsContext);

  useEffect(() => {
    const position = getPosition();
    if (position) { // 增加空值判断,避免添加无效坐标
      // 使用函数式更新,基于最新的state数组添加元素
      setTilesCoords(prevCoords => [...prevCoords, { position, key: children }]);
    }
  }, [setTilesCoords]);

  // ... 其余样式逻辑保持不变
};

额外优化建议

  • 为tilesCoords定义更具体的类型(而非object[]),提升类型安全性;
  • 确保children作为key是唯一的,避免重复添加相同Tile的坐标;
  • 考虑在组件卸载时从tilesCoords中移除对应坐标(如果需要动态增减Tile)。

内容的提问来源于stack exchange,提问作者Techoplite

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 05:27:49