react-diagrams icon indicating copy to clipboard operation
react-diagrams copied to clipboard

Infinite render loop when using DefaultLinkWidget

Open ddelpiano opened this issue 1 year ago • 0 comments

We recently started using this library and we are building a wrapper library around it that gives to the user something similar to figjam where the user can drag and drop from a side dock bar the elements needed.

So far no problem until we started using the DefaultLinkWidget in order to do our custom widget for the links. In order to do so we based our example code on the second demo link we found in this repo.

Here the widget code

import * as React from "react";
import {
    DefaultLinkWidget, LinkWidget
} from '@projectstorm/react-diagrams';

const CustomLinkArrowWidget = (props) => {
    const {point, previousPoint} = props;

    const angle =
        90 +
        (Math.atan2(
                point.getPosition().y - previousPoint.getPosition().y,
                point.getPosition().x - previousPoint.getPosition().x
            ) *
            180) /
        Math.PI;

    //translate(50, -10),
    return (
        <g className="arrow" transform={'translate(' + point.getPosition().x + ', ' + point.getPosition().y + ')'}>
            <g style={{transform: 'rotate(' + angle + 'deg)'}}>
                <g transform={'translate(0, -3)'}>
                    <polygon
                        points="0,10 8,30 -8,30"
                        fill={props.color}
                        data-id={point.getID()}
                        data-linkid={point.getLink().getID()}
                    />
                </g>
            </g>
        </g>
    );
};



export class CustomLinkWidget extends DefaultLinkWidget {
    generateArrow(point, previousPoint) {
        return (
            <CustomLinkArrowWidget
                key={point.getID()}
                point={point}
                previousPoint={previousPoint}
                colorSelected={this.props.link.getOptions().selectedColor}
                color={this.props.link.getOptions().color}
            />
        );
    }

    render() {
        //ensure id is present for all points on the path
        var points = this.props.link.getPoints();
        var paths = [];
        this.refPaths = [];

        //draw the multiple anchors and complex line instead
        for (let j = 0; j < points.length - 1; j++) {
            paths.push(
                this.generateLink(
                    LinkWidget.generateLinePath(points[j], points[j + 1]),
                    {
                        'data-linkid': this.props.link.getID(),
                        'data-point': j,
                        onMouseDown: (event) => {
                            this.addPointToLink(event, j + 1);
                        }
                    },
                    j
                )
            );
        }

        //render the circles
        for (let i = 1; i < points.length - 1; i++) {
            paths.push(this.generatePoint(points[i]));
        }

        if (this.props.link.getTargetPort() !== null) {
            paths.push(this.generateArrow(points[points.length - 1], points[points.length - 2]));
        } else {
            paths.push(this.generatePoint(points[points.length - 1]));
        }

        return <g data-default-link-test={this.props.link.getOptions().testName}>{paths}</g>;
    }
}


class CustomLinkAdapter extends React.Component {

    render() {
        const {model, engine} = this.props;
        return (
            <CustomLinkWidget link={model} diagramEngine={engine}/>
        );
    }
}

// @ts-ignore
export default CustomLinkAdapter;

and here the factory

import { MetaLinkModel } from './MetaLinkModel';
import { UnknownTypeWidget } from '../components/UnknownTypeWidget';
import { ReactDiagramMetaTypes } from '../constants';
import React from 'react';
import { DefaultLinkFactory } from '@projectstorm/react-diagrams';

export class MetaLinkFactory extends DefaultLinkFactory {
  componentsMap: Map<string, JSX.Element>;

  constructor(componentsMap: Map<string, JSX.Element>) {
    super(ReactDiagramMetaTypes.META_LINK);
    this.componentsMap = componentsMap;
  }

  generateModel() {
    return new MetaLinkModel();
  }

  generateLinkSegment(
    model: MetaLinkModel,
    selected: boolean,
    path: string
  ): JSX.Element {
    // @ts-ignore
    if (this.componentsMap.has(model.getOptions()?.shape)) {
      const ReactComponentType = this.componentsMap.get(
        // @ts-ignore
        model.getOptions().shape
      );

      return (
        // @ts-ignore
        <ReactComponentType diagramEngine={this.engine} link={model} path={path} selected={selected} />
      );
    }
    // TODO: Generate default link instead
    return <UnknownTypeWidget />;
  }
}

Few notes on the issue.

We could isolate the problem to the DefaultLinkSegmentWidget since if we hack the paths in order to render only the head of the arrow we don't have any problem with that, example down here

instead of this

return <g data-default-link-test={this.props.link.getOptions().testName}>{paths}</g>;

if we render this it works but it only render the head of the arrow, so we now know that the problem is in the line segment.

return <g data-default-link-test={this.props.link.getOptions().testName}>{paths[1]}</g>;

One more piece of information that we discovered adding the key property to the return in the factory is that react re-renders these components in loop to then complaining due the presence in the DOM of another element of the same id, so this means that we are rendering the lines no-stop. This has been noticed also before since we tried to use a shouldComponentUpdate to stop this never ending loop but we realised that we were never hitting this method but only the render because this component was always a new one.

My suspect is that this is due to the React.cloneElement used in the DefaultLinkSegmentWidget in order to generate the Top and Bottom, my main question is then why this works in the demo given and we are having this weird behaviour since I don't see that much difference in what we are doing and what done in the demo?

Many thanks for any help given.

ddelpiano avatar Jul 28 '22 12:07 ddelpiano