Is it possible to call React from an html event handler

Viewed 134

I am using the d3 drawing library with React.js, and have run into a problem with firing onClick events from SVG elements.

What I want to do is call a function (passed from parent in props) when I click on an element in the SVG. The problem is that the elements of the SVG are produced by d3, and are not react components. Is there any way that I can make this work?

For those not familiar with d3, it provides a very nice fluent syntax for generating and updating svg from a set of data. The code looks something like this:

      const existingNodes = d3Select('.nodes')
        .selectAll('g.node')
        .data(root.descendants());
      const newNodes = existingNodes.enter().append('g');
      newNodes
        .append('circle')
        .attr('onclick', (node) => 'onNodeSelected("Test")')
        .attr('r', (node) => nodeRadius(node.data));

The problem with this code is the line that says .attr('onclick', (node) => 'onNodeSelected("Test")') which correctly adds an onclick attribute to the circles, but of course onNodeSelected is a prop of the react component and can't be accessed like this.

To provide some context, the abbreviated code for the React component is like this:

export const HierarchyDiagram = ({ onNodeSelected, hierarchyData }) => {

  useEffect(() => {
    function buildSvg(root) {
    ...abbreviated
      const existingNodes = d3Select('.nodes')
        .selectAll('g.node')
        .data(root.descendants());
      const newNodes = existingNodes.enter().append('g');
      newNodes
        .append('circle')
        .attr('onclick', (node) => 'onNodeSelected({node})')
        .attr('r', (node) => nodeRadius(node.data));
    ....abbreviated
    }

    if (hierarchyData) {
      buildSvg(hierarchyData);
    }
  }, [hierarchyData]);

  return (
    <svg width="100%" height="100%">
      <g className="wrapper">
        <g className="links"></g>
        <g className="nodes"></g>
      </g>
    </svg>
  );
};
2 Answers

If you want to attach the onclick event use on. This should work:

.append('circle')
.on('click', (evt, node) => {
  onNodeSelected(node);
  d3.event.stopPropagation(); // stop the event propagation
});

I came up with a horrible solution that does work. If anyone else can find a better solution then I will mark your answer as the correct one.

My solution was to add a hidden input and use it to pass the ID of the node that was clicked. This works because the child element (circle in the SVG) receives the click before the parent (events bubble up).

My abbreviated code looks like this:

export const HierarchyDiagram = ({ onNodeSelected, hierarchyData }) => {
  const hiddenInputId = 'selected-node-id';

  useEffect(() => {
    function buildSvg(root) {
    ...abbreviated
      const existingNodes = d3Select('.nodes')
        .selectAll('g.node')
        .data(root.descendants());
      const newNodes = existingNodes.enter().append('g');
      newNodes
        .append('circle')
        .attr('onclick', (node) => 'getElementById("' + hiddenInputId + '").value="' + node.data.id + '"')
        .attr('r', (node) => nodeRadius(node.data));
    ....abbreviated
    }

    if (hierarchyData) {
      buildSvg(hierarchyData);
    }
  }, [hierarchyData]);

  function handleClick() {
    const hiddenInput = document.getElementById(hiddenInputId);
    const selectedNodeId = hiddenInput.value;
    hierarchyData.descendants().forEach((node) => {
      if (node.data?.id === selectedNodeId) onNodeSelected(node.data);
    });
  }

  return (
    <>
      <input type="hidden" id={hiddenInputId} />
      <svg width="100%" height="100%">
        <g className="wrapper">
          <g className="links"></g>
          <g className="nodes"></g>
        </g>
      </svg>
    </>
  );
};
Related