简体   繁体   中英

Multiple re-rendering of child component problem

I have a child component called Plot.js. This takes a prop plot which is an object with a property points , which contains an array of x, y points. I also have an array of files . files are looped through, and for each file a polygon is drawn on a canvas according to the points in 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:

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:

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 ). 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))); . This re-renders JUST THE INSTANCE of the plot i'm manipulating. 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. So the plots will all look identical.

This all works fine. I have created a simplified CodePen. Omitted is the re-render of JUST THE PLOT instance being manipulated on mouse move (as this works fine). 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

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:

在此处输入图像描述

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. Therefore, performance is very important. Why is each instance re-rendering 3 times? 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. 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. After dragging the polygon and on mouse up, you can see each instance of the Plot component reloads 3 times instead of once:

在此处输入图像描述

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). Im beginning to think that, fundamentally, this is a limitation of React itself? Code Pen is here: 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. 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.

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.

Reading the pen will explain better. 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. You should compare the contents of these objects instead of their object reference.

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.

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:

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.

Briefly describe the modified steps:

  1. You pass plot props from the Parent as coordinate data for the Plot component.
  2. Through the onEditGate callback function, let the Parent component get the information after the coordinate movement, and then modify the coordinate value.
  • Result: Each Plot component is updated once, and the code becomes cleaner.
    在此处输入图像描述 (You can check the full display at the bottom (CodeSandbox).)

Code fragment (modified slightly from your code)

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;

Full code sample (modified slightly from your code)

编辑 TextField selectionStart

Hope to help you:)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM