Accepted answer

You either precompute/measure your typeface geometry and get a reasonable estimate of the text dimensions based on the input string (this is the simplest solution, but will obviously break if the typeface changes), or perform a two-stage rendering:

that is, you obtain the dom element via the ref, fetch the box on mount and finally re-render by updating state, something like:

class MyLabel extends React.Component {
    this.state = {text_extents:null};
  componentDidMount() {
   const box = this.text.getBBox();

 render() {
   const margin = 2;
   const extents = this.state.text_extents;
   const label = <text ref={(t) => { this.text = t; }} textAnchor="middle" dy={extents?(extents[1]/4):0} >{this.props.children}</text>;
   const outline = extents ?
         <rect x={-extents[0]/2-margin} y={-extents[1]/2-margin} width={extents[0]+2*margin} height={extents[1]+2*margin} className="labeloutline"></rect>
         : null;

   return <g transform={`translate(${this.props.x},${this.props.y}) rotate(${this.props.angle})`}>{outline}{label}</g>;

Note that, as per latest react docs, this should not incur in any user visible flickering:

componentDidMount(): Calling setState() in this method will trigger an extra rendering, but it will happen before the browser updates the screen. This guarantees that even though the render() will be called twice in this case, the user won’t see the intermediate state. Use this pattern with caution because it often causes performance issues. It can, however, be necessary for cases like modals and tooltips when you need to measure a DOM node before rendering something that depends on its size or position.

Finally, note that if the label string changes (via props or whatever) you'll need to update extents accordingly (via componentDidUpdate()).


// MyLabel should be centred at x,y, rotated by angle, 
// and have a bounding box around it, 2px from the text.
class MyLabel extends React.Component {
width: 40,
height: 12,
angle: this.props.angle,
componentDidMount() {
 var reactDomElem = this.label.getBBox()
 angle: this.state.angle,
  render() {
    const label = <text ref={(ref)=>this.label = ref} x={this.state.x} y={this.state.y} textAnchor="middle" alignmentBaseline="baseline">{this.props.children}</text>;
    // label isn't a DOM element, so you can't call label.getBoundingClientRect() or getBBox()

    // (Magic happens here to find bbox of label..)        
    // make up a static one for now
    let bb = {x: this.state.x, y: this.state.y, width:this.state.width, height: this.state.height};
    // add margin
    const margin = 2;
    bb.width += margin * 2;
    bb.height += margin * 2;
    bb.x -= this.state.width/2;
    bb.y -= this.state.height/2 + margin*2;
    // rect uses bbox to decide its size and position
    const outline = <rect x={bb.x} y={bb.y} width={bb.width} height={bb.height} className="labeloutline"></rect>;
    const rot = `rotate(${this.state.angle} ${this.state.x} ${this.state.y})`;
    // build the final label (plus an x,y spot for now)
    return <g  transform={rot}>{outline}{label}<circle cx={this.state.x} cy={this.state.y} r="2" fill="red" /></g>;

class Application extends React.Component {
  render() {
    return <svg width={300} height={300}>
      <MyLabel x={100} y={100} angle={0}>Dalmation</MyLabel>
      <MyLabel x={200} y={100} angle={45}>Cocker Spaniel</MyLabel>
      <MyLabel x={100} y={200} angle={145}>Pug</MyLabel>
      <MyLabel x={200} y={200} angle={315}>Pomeranian</MyLabel>

 * Render the above component into the div#app
ReactDOM.render(<Application />, document.getElementById('app'));
body { background: gray; }
svg {background: lightgray;}
.labeloutline { fill: white; stroke: black;}
<script src=""></script>
<script src=""></script>

<div id="app"></div>

Related Query

More Query from same tag