xyflow / xyflow

React Flow | Svelte Flow - Powerful open source libraries for building node-based UIs with React (https://reactflow.dev) or Svelte (https://svelteflow.dev). Ready out-of-the-box and infinitely customizable.
https://xyflow.com
MIT License
24.58k stars 1.58k forks source link

Edges do not render initially, 'Couldn't create edge for source handle id: "null"' when using ELK #4537

Open lucas-amberg opened 1 month ago

lucas-amberg commented 1 month ago

What platform were you using when you found the bug?

Live code example

No response

Describe the Bug

When using ELK js, all of my nodes render in a tree format as expected, and the edges update properly using setEdges, but they do not render and display the following error:

image_2024-08-12_100021532

(4 edges logged, expected, but not rendering),

However when I make a change to my code and save, the edges render propertly 🤔

image_2024-08-12_100149815

And this only happens when using the formattedNodes from ELK, if I use my nodes from before formatting, it does work as expected, just without the tree formatting I look to achieve from ELK. Just to note, neither the ELK formatted edges or the initial edges work in this scenario, both throw the same error, so it is likely related to ELK and the Nodes rather than the edges.

image_2024-08-12_100651656

Code:

Here is some of the code, if anyone needs any more let me know.

Here is my useEffect after fetching the data

        let orgMemberRelationships: Edge[] = [];

        orgMemberSupervisors.forEach((supervisor) => {
            Object.values(supervisor["-SUPERVISOR_TO->User"]!).forEach(
                (supervised) => {
                    orgMemberRelationships.push({
                        id: supervisor.nodeId + "->" + supervised.nodeId,
                        source: supervisor.nodeId,
                        target: supervised.nodeId,
                        sourceHandle: 'a',
                        ...edgeStyles,
                    });
                },
            );
        });

        const orgMemberNodes = orgDataArray.map((orgMember, idx) => {
            const depth = Math.floor(Math.log2(idx + 1.5));
            const positionInLevel = idx - (Math.pow(2, depth) - 1);

            return {
                id: orgMember.nodeId,
                type: "orgMember",
                data: orgMember,
                position: {
                    x: 100 + positionInLevel * 400 * Math.pow(-1, idx),
                    y: 400 + depth * 400,
                },
            };
        });

        // Use an async function inside useEffect to handle the Promise
        const applyLayout = async () => {
            try {
                const { nodes: layoutedNodes, edges: layoutedEdges } =
                    await getLayoutedGraph(
                        orgMemberNodes,
                        orgMemberRelationships,
                    );
                    setNodes(layoutedNodes as Node[])
                    console.log(layoutedNodes)
            } catch (error) {
                console.error("Error applying layout:", error);
            }
        };

        applyLayout().then(() => {
            console.log('applying');
            setTimeout(() => {
                applyEdges(orgMemberRelationships);
            }, 3000);
        });

My ELK formatting function


const elk = new ELK();

const elkOptions = {
    "elk.algorithm": "layered",
    "elk.layered.spacing.nodeNodeBetweenLayers": "100",
    "elk.spacing.nodeNode": "80",
    "elk.direction": "DOWN",
};

const getLayoutedGraph = async (nodes: Node[], edges: Edge[]) => {
    const graph = {
        id: "root",
        layoutOptions: elkOptions,
        children: nodes.map((node) => ({
            ...node,
            // Adjust the target and source handle positions based on the layout
            // direction.

            // Hardcode a width and height for elk to use when layouting.
            width: 350,
            height: 250,
            sourcePosition: "bottom",
            targetPosition: "top",
        })),
        edges: edges.map((edge) => {
            return {
                ...edge,
                sourceHandle: 'a',
                sources: [edge.source],
                targets: [edge.target],
            };
        }),

    };

    const layoutedGraph = await elk.layout(graph);

    if (layoutedGraph) {
        return {
            nodes: layoutedGraph.children!.map((node) => ({
                ...node,
                // React Flow expects a position property on the node instead of `x`
                // and `y` fields.
                position: { x: node.x, y: node.y },
            })),

            edges: layoutedGraph.edges,
        };
    } else {
        return { nodes, edges };
    }
};

Additionally, I've tried manually setting the node handle Ids and that did not make any difference.

Steps to reproduce the bug or issue

  1. Create a set of nodes and edges
  2. Format using ELK js
  3. Call setNodes and setEdges

Expected behavior

I expected all of the edges to render properly when setEdges is called, not with a change to the code.

Screenshots or Videos

No response

Additional context

No response

peterkogo commented 1 month ago

Can show us your edges that you are ultimately passing to React Flow, that produces this error? Also, is it possible you are only rendering your components in a delayed fashion?

bcakmakoglu commented 1 month ago

Your code suggests that you assign a as the source handle id to edges:

orgMemberRelationships.push({
    id: supervisor.nodeId + "->" + supervised.nodeId,
    source: supervisor.nodeId,
    target: supervised.nodeId,
    sourceHandle: 'a', // <---- source handle id `a` is assigned here
    ...edgeStyles,
});

Any chance your <Handle> component uses a different id than a or possibly has no id assigned at all?

lucas-amberg commented 1 month ago

Yes @bcakmakoglu , I assigned the source <Handle? the id of "a" and the edges that id because I was assuming leaving it unassigned was what was throwing that "null" error

lucas-amberg commented 1 month ago

Can show us your edges that you are ultimately passing to React Flow, that produces this error? Also, is it possible you are only rendering your components in a delayed fashion?

Not sure if this is what you are looking for, but here are the edges after they are set using setEdges and logged on the re render

image_2024-08-12_103213423

bcakmakoglu commented 1 month ago

I assigned the source <Handle? the id of "a" and the edges that id because I was assuming leaving it unassigned was what was throwing that "null" error

I didn't quite understand that. Can you show us an excerpt of your <Handles>? Both source and target ones?

Leaving it unassigned is perfectly fine, given that you only use one <Handle> of each type (source or target). You only ever need an id if there are multiple handles of the same type.

lucas-amberg commented 1 month ago

I didn't quite understand that. Can you show us an excerpt of your <Handles>? Both source and target ones?

Leaving it unassigned is perfectly fine, given that you only use one <Handle> of each type (source or target). You only ever need an id if there are multiple handles of the same type.

Source Handle:

<Handle
      style={{ background: "transparent" }}
      type="source"
      id="a"
      position={Position.Bottom}
  />

Target handle:

<Handle
      type="target"
      style={{ background: "transparent" }}
      id="b"
      position={Position.Top}
  />
bcakmakoglu commented 1 month ago

Mh... Any conditions on rendering these Handles? Also any luck if you just remove these ids since they're unnecessary anyway? ^^

lucas-amberg commented 1 month ago

There's no conditions, they always render no matter what. No luck with removing the Ids unfortunately, I added those in there hoping to fix this issue so they aren't the cause. I managed to swap out ELK for dagre and i'm getting better results so unless you guys want to keep iterating on this we can move on from it, thank you for the help 🫡

moklick commented 1 month ago

I would really like to understand why this is not working for you. Any chance we could check out the repo or could you create a codesandbox?

lucas-amberg commented 1 month ago

@moklick I'll try to make a codesandbox if I get the chance

moklick commented 2 weeks ago

that sounds great @lucas-amberg