[英]Multiple re-rendering of child component problem
I have a child component called Plot.js.我有一个名为 Plot.js 的子组件。 This takes a prop
plot
which is an object with a property points
, which contains an array of x, y points.这需要一个道具
plot
,它是一个 object 属性points
,它包含一个 x,y 点数组。 I also have an array of files
.我还有一组
files
。 files
are looped through, and for each file a polygon is drawn on a canvas according to the points in plot.gate.points
:循环遍历
files
,并根据 plot.gate.points 中的点在plot.gate.points
上绘制每个文件的多边形:
The Parent component looks like this:父组件如下所示:
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = {
plots: plots.plotList,
files: files.fileList
}
this.onEditGate = this.onEditGate.bind(this);
}
onEditGate = (change) => {
this.state.plots[change.plotIndex] = change.plot;
console.log("setting the state");
this.setState({
files: files,
plots: this.state.plots
});
};
render() {
return (
<table className="workspace">
<tbody>
{files.fileList.map((file, fileIndex) => {
return (
<tr key={`tr-${fileIndex}`}>
{plots.plotList.map((plot, plotIindex) => {
return (
<td key={`td-${plotIindex}`}>
<Plot
plotIndex={`${fileIndex}-${plotIindex}`}
plot={plot}
file={file}
onEditGate={this.onEditGate}
></Plot>
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
);
}
}
files
and plots
are: files
和plots
是:
let plots = JSON.parse('{"plotList":[{"population":"All","gate":{"points":[[10,10],[70,10],[70,70],[10,70]]}}]}');
let files = JSON.parse('{"fileList":[{"file":"a"},{"file":"b"}]}');
And Plot is:而 Plot 是:
function Plot(props) {
console.log('in function Plot, plotIndex is ' + props.plotIndex + ', props.plot points are ', props.plot.gate.points);
const [localPlot, setLocalPlot] = React.useState(props.plot);
React.useEffect(() => {
setLocalPlot(props.plot);
console.log('in useEffect, plotIndex is ' + props.plotIndex + ', localPlot points are ', localPlot.gate.points);
const context = getContext(props.plotIndex);
context.clearRect(0, 0, 200, 200);
context.fillStyle = "white";
drawPolygonLine(context, localPlot);
}, [localPlot, props.plot, props.file]);
const drawPolygonLine = (context, plot) => {
context.strokeStyle = "red";
context.lineWidth = 1;
context.beginPath();
// draw the first point of the gate
context.moveTo(plot.gate.points[0][0], plot.gate.points[0][1]);
plot.gate.points.forEach((pointOnCanvas) => {
context.lineTo(pointOnCanvas[0], pointOnCanvas[1]);
});
context.closePath();
context.stroke();
};
const movePolygon = (plotIndex) => {
localPlot.gate.points = props.plot.gate.points.map((point) => {
return [point[0] + 40, point[1] + 40];
});
setLocalPlot(JSON.parse(JSON.stringify(localPlot)));
let change = {
type: "EditPolygon",
plot: localPlot,
plotIndex: props.plotIndex.split("-")[1],
points: JSON.parse(JSON.stringify(localPlot.gate.points)),
};
props.onEditPolygon(change);
}
return (
<div>
<canvas
style={{ border: "thick solid #32a1ce" }}
className="canvas"
id={`canvas-${props.plotIndex}`}
width={200}
height={200}
/>
<button
onClick={() => movePolygon(props.plotIndex)}
>
Move plot {props.plotIndex}
</button>
</div>
);
}
I need to make a copy of plot
(called localPlot
).我需要复制
plot
(称为localPlot
)。 This is because a user can manipulate a polygon on one of the plot. As the mouse moves, I get the new points and then do setLocalPlot(JSON.parse(JSON.stringify(localPlot)));
这是因为用户可以在 plot 之一上操作多边形。当鼠标移动时,我得到新的点,然后执行
setLocalPlot(JSON.parse(JSON.stringify(localPlot)));
. . This re-renders JUST THE INSTANCE of the plot i'm manipulating.
这仅重新呈现我正在操作的 plot 的实例。 On mouse up, I propagate the final polygon points back up to the State, and re-render everything, looping through files and creating the plots with the new polygon points.
在鼠标向上时,我将最终的多边形点传播回 State,并重新渲染所有内容,遍历文件并使用新的多边形点创建绘图。 So the plots will all look identical.
所以这些图看起来都一样。
This all works fine.这一切都很好。 I have created a simplified CodePen.
我创建了一个简化的 CodePen。 Omitted is the re-render of JUST THE PLOT instance being manipulated on mouse move (as this works fine).
省略了在鼠标移动时操作的仅 PLOT 实例的重新渲染(因为这工作正常)。 Instead, I've added a "Move" button to show what happens when I propagate back up to the State. Code pen is here: https://codepen.io/mark-kelly-the-looper/pen/WNdrRBj?editors=0011
相反,我添加了一个“移动”按钮来显示当我传播回 State 时会发生什么。代码笔在这里: https://codepen.io/mark-kelly-the-looper/pen/WNdrRBj?editors =0011
However, when I look at the console.log outputs, I see that after updating the State, each Plot component instance re-renders 3 times, not just once as I expected, see code pen logs:但是,当我查看 console.log 输出时,我看到在更新 State 之后,每个 Plot 组件实例重新呈现 3 次,而不是我预期的一次,请参阅代码笔日志:
The problem is my app does a huge amount of work within useEffect()
, calculating the position of thousands of x, y points, as well as the position of the polygon.问题是我的应用程序在
useEffect()
中做了大量工作,计算了数千个 x、y 点的 position,以及多边形的 position。 Therefore, performance is very important.因此,性能非常重要。 Why is each instance re-rendering 3 times?
为什么每个实例重新渲染 3 次? Is there a better approach?
有更好的方法吗?
EDIT: The reason I have localPlot
is as a user drags, I need to re-render just the instance of the Plot AND i need useEffect()
to be trigger, because within useEffect()
is where all the drawing on the canvas takes place.编辑:我有
localPlot
的原因是用户拖动,我需要重新渲染 Plot 的实例,我需要useEffect()
来触发,因为useEffect()
是 canvas 上所有绘图发生的地方. If theres another way to show the polygon being dragged, then Im all ears...如果有另一种方式来显示被拖动的多边形,那么我洗耳恭听......
EDIT I have added a more comprehensive Code Pen here where you can see the re-rendering of a Plot instance on dragging of the polygon.编辑我在这里添加了一个更全面的代码笔,您可以在其中看到在拖动多边形时重新渲染 Plot 实例。 After dragging the polygon and on mouse up, you can see each instance of the Plot component reloads 3 times instead of once:
拖动多边形并在鼠标上移后,您可以看到 Plot 组件的每个实例重新加载 3 次而不是一次:
As mentioned above, this is not acceptable for my application as within useEffect()
of each Plot
instance I do thousands of calculations (not included here).如上所述,这对我的应用程序来说是不可接受的,因为在每个
Plot
实例的useEffect()
中我进行了数千次计算(此处未包括在内)。 Im beginning to think that, fundamentally, this is a limitation of React itself?我开始认为,从根本上说,这是 React 本身的局限性? Code Pen is here: https://codepen.io/mark-kelly-the-looper/pen/bGYzZQR?editors=0011 .
代码笔在这里: https://codepen.io/mark-kelly-the-looper/pen/bGYzZQR?editors=0011 。
You are setting localPlot in the effect that depends on localPlot, so it's always going to render at least twice.您在依赖于 localPlot 的效果中设置 localPlot,因此它总是至少要渲染两次。 Usually that's an infinite loop.
通常这是一个无限循环。
You also have additional dependencies in this effect that are not used.您还具有未使用的此效果的其他依赖项。 Removing them removes unnecessary renders.
删除它们会删除不必要的渲染。
React.useEffect(() => {
console.log('in useEffect', Date.now());
const context = getContext(props.plotIndex);
context.clearRect(0, 0, 200, 200);
context.fillStyle = "white";
drawGateLine(context, localPlot);
}, [localPlot]);
Okay I went through the spiderweb of renders and got the drawing effect to run only once for each instance when the propagation occurs.好吧,我浏览了渲染的蜘蛛网,并让绘图效果在传播发生时为每个实例只运行一次。
I had to update the setStates too to remove all the mutation.我也必须更新 setStates 以删除所有突变。
Every comment above is still correct;上面的每个评论仍然是正确的; there is never a need to update an effect's dependency in that effect.
永远不需要更新效果对该效果的依赖性。 This is always a mistake.
这总是一个错误。
Because of the way the offset state is on the module (so React doesn't know about it), you had to trick React into thinking things had updated, but the flow was very circular.由于偏移量 state 在模块上的方式(因此 React 不知道它),你不得不欺骗 React 认为事情已经更新,但流程非常循环。
Reading the pen will explain better.读笔会更好地解释。 https://codepen.io/windowsill/pen/BaJzRmY
https://codepen.io/windowsill/pen/BaJzRmY
When you put localPlot and props.plot in the useEffect's dependency list, React will do a SHALLOW comparison of these variables with their previous values after each render to decide whether to run the code inside the hook.当您将 localPlot 和 props.plot 放入 useEffect 的依赖列表中时,React 将在每次渲染后将这些变量与它们之前的值进行浅层比较,以决定是否运行钩子内的代码。 You should compare the contents of these objects instead of their object reference.
您应该比较这些对象的内容,而不是它们的 object 引用。
Also, the useEffect hook is run AFTER each render, not before it, and thus setting state with setState in a useEffect hook will always cause an extra render, so it's better not to put that in there.此外,useEffect 钩子在每次渲染之后运行,而不是在渲染之前运行,因此在 useEffect 钩子中使用 setState 设置 state 总是会导致额外的渲染,所以最好不要把它放在那里。
The solution is to do a deep comparison of localPlot and props.plot on each render, outside of your useEffect hook, and only update localPlot if they are different:解决方案是在 useEffect 挂钩之外的每个渲染器上对 localPlot 和 props.plot 进行深入比较,并且仅在它们不同时更新 localPlot:
const plotsAreSame = (plot1, plot2) => {
if (plot1.population != plot2.population) return false;
return (plot1.gate.points.length===plot2.gate.points.length && plot1.gate.points.every(
(p,i)=>(p[0]===plot2.gate.points[i][0] && p[1]===plot2.gate.points[i][1])
))
}
function Plot(props) {
console.log('in function Plot, plotIndex is ' + props.plotIndex + ', props.plot points are ', props.plot.gate.points);
const [localPlot, setLocalPlot] = React.useState(props.plot);
if (!plotsAreSame(localPlot, props.plot)) {setLocalPlot(props.plot)}
React.useEffect(() => {
console.log('in useEffect, plotIndex is ' + props.plotIndex + ', localPlot points are ', localPlot.gate.points);
const context = getContext(props.plotIndex);
context.clearRect(0, 0, 200, 200);
context.fillStyle = "white";
drawGateLine(context, localPlot);
}, [localPlot, props.plotIndex]);
This way the two components will only be rendered once.这样两个组件只会被渲染一次。
You don't really need the localState
state, it just complicates your process.您实际上并不需要
localState
state,它只会使您的流程复杂化。
Briefly describe the modified steps:简述修改步骤:
plot
props from the Parent
as coordinate data for the Plot
component.Parent
传递plot
道具作为Plot
组件的坐标数据。onEditGate
callback function, let the Parent
component get the information after the coordinate movement, and then modify the coordinate value.onEditGate
回调function,让Parent
组件获取到坐标移动后的信息,进而修改坐标值。Plot
component is updated once, and the code becomes cleaner.Plot
组件更新一次,代码变得更干净。function Plot({ plotIndex, plot, file, onEditGate = () => {} }) {
console.log(
"in function Plot, plotIndex is " + plotIndex + ", props.plot points are ",
plot.gate.points
);
useEffect(() => {
console.log(
"in useEffect, plotIndex is " + plotIndex + ", localPlot points are ",
plot.gate.points
);
const context = getContext(plotIndex);
context.clearRect(0, 0, 200, 200);
context.fillStyle = "white";
drawGateLine(context, plot);
}, [plot, plotIndex]);
const drawGateLine = (context, plot) => {
context.strokeStyle = "red";
context.lineWidth = 1;
context.beginPath();
// draw the first point of the gate
context.moveTo(plot.gate.points[0][0], plot.gate.points[0][1]);
plot.gate.points.forEach((pointOnCanvas) => {
context.lineTo(pointOnCanvas[0], pointOnCanvas[1]);
});
context.closePath();
context.stroke();
};
const movePolygon = (plotIndex) => {
const saveCopyLocalPlot = JSON.parse(JSON.stringify(plot));
const newLocal = saveCopyLocalPlot.gate.points.map((point) => {
return [point[0] + 40, point[1] + 40];
});
saveCopyLocalPlot.gate.points = newLocal;
let change = {
type: "EditGate",
plot: saveCopyLocalPlot,
plotIndex: plotIndex.split("-")[1],
points: JSON.parse(JSON.stringify(saveCopyLocalPlot.gate.points))
};
onEditGate(change);
};
return (
<div>
<canvas
style={{ border: "thick solid #32a1ce" }}
className="canvas"
id={`canvas-${plotIndex}`}
width={200}
height={200}
/>
<button onClick={() => movePolygon(plotIndex)}>
Move plot {plotIndex}
</button>
</div>
);
}
export default Plot;
Hope to help you:)希望能帮到你:)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.