React中处理依赖自身的状态:多边形编辑元数据继承问题
React状态处理:多边形元数据同步与状态依赖问题
功能需求
- 支持用户绘制、编辑顶点,顶点可自由组合为多边形或独立存在
- 多边形创建后可添加元数据(metadata)
- 当边分割多边形为两个时,较大多边形的元数据需被新生成的两个多边形继承
- 所有多边形修改操作中,元数据需持续保留
当前困境
计划维护4个状态:vertices、edges、polygons、metadata。当edges或vertices变更时,通过算法从边求解多边形,再为新多边形匹配/创建元数据,但此逻辑无法通过useState/useMemo实现:
useMemo是纯计算逻辑,不能执行更新状态的副作用- 元数据匹配逻辑依赖
metadata当前状态,会形成状态依赖自身的循环矛盾
多边形求解和元数据匹配算法已完成,但无法契合React状态范式。虽知道useEffect可实现,但希望遵循官方规范找到更合理的实现方式。
另外还有一个小问题:如何保证addVertex方法在批量添加顶点时不会出错?
精简代码示例
import React, { useState } from "react"; import { Stage, Layer, Circle, Group, Line, Text } from "react-konva"; import konva from "konva"; import "./App.css"; import { polygonsFromEdges } from "./polygonSolver"; type Vertex = { x: number; y: number; index: number }; type Edge = { vertices: number[]; index: number }; const App = () => { const [vertices, setVertices] = React.useState([] as Vertex[]); const [edges, setEdges] = React.useState([] as Edge[]); const polygons = React.useMemo(getPolygons, [edges, vertices]); const [selectedVertex, setSelectedVertex] = useState<number | null>(null); function getPolygons() { // 转换为polygonsFromEdges所需格式 const rawEdges = edges.map((edge) => edge.vertices) as [number, number][]; const rawVertices = vertices.map((vertex) => [vertex.x, vertex.y]) as [ number, number ][]; // rawPolygons: 多边形顶点索引的有序列表 const rawPolygons: number[][] = polygonsFromEdges(rawVertices, rawEdges); return rawPolygons.map((rawPolygon, idx) => { // 转换回App组件所需格式 const flatCoordinates = rawPolygon .map((vertexId) => [vertices[vertexId].x, vertices[vertexId].y]) .flat(); return { coordinates: flatCoordinates, vertices: rawPolygon, index: idx, }; }); } function onClickStage(evt: konva.KonvaEventObject<MouseEvent>) { const clickedOnObject = evt.target.parent !== null; if (clickedOnObject) return; const vertex = addVertex(evt.evt.x, evt.evt.y); if (selectedVertex !== null) { addEdge(selectedVertex, vertex.index); } setSelectedVertex(vertex.index); } function addVertex(x: number, y: number): Vertex { // 小问题:批量添加顶点时如何避免出错? const vertexIndex = vertices.length; const newVertex = { x, y, index: vertexIndex }; setVertices([...vertices, newVertex]); return newVertex; } function addEdge(vertex1Idx: number, vertex2Idx: number) { const newEdge: Edge = { vertices: [vertex1Idx, vertex2Idx], index: edges.length, }; setEdges(edges.concat(newEdge)); } function onClickVertex(evt: konva.KonvaEventObject<MouseEvent>) { const index = parseInt(evt.currentTarget.id(), 10); const clickedVertex = vertices[index]; if (selectedVertex !== null) { addEdge(selectedVertex, clickedVertex.index); } setSelectedVertex(clickedVertex.index); evt.cancelBubble = true; } return ( <Stage width={window.innerWidth} height={window.innerHeight} onClick={onClickStage} > <Layer> {polygons.map((polygon) => ( <Group key={polygon.index.toString()}> <Line points={polygon.coordinates} closed fill="#f005" /> </Group> ))} {edges.map((edge) => ( <Group key={edge.index} id={edge.index.toString()}> <Line stroke="red" strokeWidth={1} points={edge.vertices .map((vertexIdx) => [ vertices[vertexIdx].x, vertices[vertexIdx].y, ]) .flat()} /> <Line stroke="#f005" strokeWidth={8} points={edge.vertices .map((vertexIdx) => [ vertices[vertexIdx].x, vertices[vertexIdx].y, ]) .flat()} /> </Group> ))} {vertices.map((vertex) => ( <Group key={vertex.index} id={vertex.index.toString()} x={vertex.x} y={vertex.y} onClick={onClickVertex} > <Text text={`${vertex.index}`} x={15} y={-6} /> <Circle radius={5} fill="#0f0" /> <Circle radius={15} fill="#0f05" /> </Group> ))} </Layer> </Stage> ); }; export default App;
解决方案建议
1. 重构状态:将metadata与多边形绑定,避免独立状态
不要把polygons和metadata拆成两个独立状态,而是让每个多边形对象自带元数据,同时用一个映射表存储多边形唯一标识到元数据的关联,避免状态同步问题。
具体步骤:
- 定义带元数据的多边形类型:
type PolygonWithMeta = { coordinates: number[]; vertices: number[]; index: number; metadata: Record<string, any>; // 元数据结构根据需求自定义 key: string; // 多边形唯一标识 }; - 维护
polygonMetadataMap状态,存储唯一标识到元数据的映射:const [polygonMetadataMap, setPolygonMetadataMap] = useState<Record<string, any>>({}); - 在多边形计算函数中,为每个新多边形匹配元数据:
function getPolygons() { // ... 原有多边形坐标计算逻辑 ... return rawPolygons.map((rawPolygon, idx) => { // 生成唯一标识:顶点索引排序后拼接,避免顶点顺序变化导致标识失效 const polygonKey = [...rawPolygon].sort().join(','); // 获取已有元数据,无则用默认值 const metadata = polygonMetadataMap[polygonKey] || {}; const flatCoordinates = rawPolygon .map((vertexId) => [vertices[vertexId].x, vertices[vertexId].y]) .flat(); return { coordinates: flatCoordinates, vertices: rawPolygon, index: idx, metadata, key: polygonKey }; }); } const polygonsWithMeta = useMemo(getPolygons, [edges, vertices, polygonMetadataMap]); - 更新元数据时,通过唯一标识操作映射表:
function updatePolygonMetadata(polygonKey: string, newMeta: Record<string, any>) { setPolygonMetadataMap(prev => ({ ...prev, [polygonKey]: {...prev[polygonKey], ...newMeta} })); }
2. 处理多边形分割时的元数据继承
分割逻辑涉及状态更新,属于副作用,用useEffect处理完全符合React规范。需要对比新旧多边形集合,找出被分割的父多边形,再将其元数据继承给新生成的子多边形:
// 用ref存储上一次的多边形集合,用于对比 const prevPolygonsRef = useRef<PolygonWithMeta[]>([]); useEffect(() => { prevPolygonsRef.current = polygonsWithMeta; }, [polygonsWithMeta]); useEffect(() => { const newPolygons = polygonsWithMeta; const oldPolygons = prevPolygonsRef.current; // 调用你的算法找出被分割的父多边形(比如通过面积对比、顶点重叠度判断) const parentPolygon = findSplitParentPolygon(oldPolygons, newPolygons); if (!parentPolygon) return; const parentMeta = polygonMetadataMap[parentPolygon.key]; if (!parentMeta) return; // 为新生成的子多边形赋值元数据 setPolygonMetadataMap(prev => { const updatedMap = {...prev}; newPolygons.forEach(poly => { if (!prev[poly.key]) { updatedMap[poly.key] = {...parentMeta}; } }); // 父多边形被分割后不存在,删除其元数据(可选) delete updatedMap[parentPolygon.key]; return updatedMap; }); }, [polygonsWithMeta, polygonMetadataMap]);
3. 解决批量添加顶点的问题
当前addVertex依赖vertices.length生成索引,批量调用时会因闭包捕获旧状态导致索引重复。改用函数式更新即可解决:
// 单个添加,支持异步获取新顶点 function addVertex(x: number, y: number): Promise<Vertex> { return new Promise(resolve => { setVertices(prev => { const newIndex = prev.length; const newVertex = { x, y, index: newIndex }; resolve(newVertex); return [...prev, newVertex]; }); }); } // 批量添加,一次更新状态 function addMultipleVertices(verticesData: {x: number, y: number}[]) { setVertices(prev => { return [...prev, ...verticesData.map((data, idx) => ({ ...data, index: prev.length + idx }))]; }); }
内容的提问来源于stack exchange,提问作者bastien girschig
相关产品推荐
相关产品推荐

