Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best way to retrieve and cache bounding boxes of SVG nodes with React.JS

I am rendering a SVG component with labels. Those label components need to be correctly laid out depending on their text content (and so their size) in order to avoid overlapping themselves.

To get the real size of each labels, it seems a double-render is needed each time a label content is updated.

At the label component level, I need to

  • render it for the first time
  • retrieve the bounding box of the real SVG DOM node
  • cache the bounding box for performance reason
  • re-render the component to adjust the label position according to its cached bounding box

Then, at each redraw :

  • render according to the cached bounding box
  • compare label content between previous and updated props and if changed:
  • update and cache the label bounding box
  • re-render according to the updated and cached bounding box

So far, here is how I have implemented my label component:

var Label = React.createClass({

  updateBBox: function() {
    // Trigger re-rendering
    this.setState({
      bbox: this.getDOMNode().getBBox()
    });
  },

  componentDidMount: function() {
    // Will trigger a re-rendering at mount
    this.updateBBox();
  },

  componentDidUpdate: function(prevProps, prevState) {
    // If content has changed, re-render
    if (this.props.content !== prevProps.content) {
      this.updateBBox();
    }
  },

  render: function() {
    // Render according to size from current bounding box if any cached
    // ...
  }

});

So, my question is not about the algorithm but if there is a better React-compliant way to implement this. Is using the state for caching this sort of slow 'computations' or DOM accesses allowed in the React way? Is double-rendering an unusual case for React?

like image 947
gentooboontoo Avatar asked Oct 07 '14 10:10

gentooboontoo


1 Answers

My personal opinion is that you should put all state that matters to you into the state of a component. So yes, the way you propose is the right way to do it.

As far as I know, double renders are the only way to get the size of a block as state, since the block needs to be drawn first. Since React is so performant, this is usually no problem. if it is, you use PureRenderMixin or other checks on shouldComponentUpdate within the children to ensure performance.

Note however that it might be smart to write this as a mixin, because of "don't repeat yourself".

I do responsive SVG stuff quite frequently so I often need x, y width and height from a block element (svg or html).

I've written this little mixin called BoundingRectAware:

var shallowEqual = require('react/lib/shallowEqual');

// keep width and height of an element. You must make a "boundingRectTarget" ref
// to the element you would like to track
module.exports = {
    getInitialState: function() {
        return {rect: {
            left: null, top: null, right: null, bottom: null, width: null, height: null
        }};
    },
    componentDidMount: function() {
        window.addEventListener("resize", this.updateDimensions);
        this.updateDimensions();
    },
    componentWillUnmount: function() {
        window.removeEventListener("resize", this.updateDimensions);
    },
    componentWillReceiveProps: function() {
        this.updateDimensions();
    },
    updateDimensions: function() {
        if (this.refs.boundingRectTarget) {
            var rect = this.refs.boundingRectTarget.getDOMNode().getBoundingClientRect();
            if (!shallowEqual(this.state.rect, rect)) {
                this.setState({rect: rect});
            }
        }
    }
};

this can be used like so:

var FooComponent = React.createClass({
    mixins: [BoundingRectAware],
    render: function() {
        return <div className="foo-class" ref="boundingRectTarget"/>;
    }
});

within FooComponent, you will automatically get the boundingClientRect as state. I would implement something alike if you use getBBox() in more than one place.

like image 131
Retozi Avatar answered Sep 29 '22 02:09

Retozi