ChrisM
ChrisM

Reputation: 2248

Third-party DOM manipulation in React

I am currently in the process of porting a legacy BackboneJS app to ReactJS. The app uses VexFlow, the JavaScript music notation rendering engine. One of the major issues I have encountered is that VexFlow renders everything itself, to SVG, in a similar fashion to D3. There is a lot of info on combining D3 with React, and I am following what seems to be the general practice in that case of using an empty React ref element as the target for my VexFlow rendering, which all takes place in componentDidMount:

export default class ScoreComponent extends React.Component {

  constructor(props) {
    super(props);
    // Create a 'ref' object, which allows us to reference the React DOM
    // element we create in the render method.
    this.scoreElem = React.createRef();
    ...
  }

  componentDidMount() {
    var score = this.score;
    var elem = this.scoreElem.current;
    score.setElem(elem).render(); // <- All VexFlow rendering happens here...
    ...
  }

  render() {
    return (
      <div className="score" id={this.props.scoreId} ref={this.scoreElem}></div>
    );
  }

}

Although this works, it makes me rather uncomfortable, especially as I have to also add a fair amount of jQuery code to handle user interaction on SVG elements (clicking and dragging complex path objects, for example), none of which React will be aware of.

So my question is: am I heading down a path that will result in me getting burned? I really like React, and am eager to say goodbye to Backbone. I was able to port most of my UI code painlessly in a weekend. I have looked at Angular in the past, but it seems way too complex, and opinionated.

Upvotes: 5

Views: 2740

Answers (1)

Ori Drori
Ori Drori

Reputation: 191976

You are heading in the right direction. When you need to use an external non react DOM library to render stuff in react, this is the way to go:

  1. Create a ref to a DOM element in the constructor.
  2. Start the plugins instance on componentDidMount(), and add reference to the plugin's instance(s) as properties on the component's instance. This will enable you to call the instance's methods from other methods.
  3. React to prop changes in componentDidUpdate(). Use the reference(s) to the plugin's instance to update it.
  4. In componentWillUnmount() clear everything the plugin added/scheduled/etc... such as event listeners, timeouts/intervals, DOM nodes created outside of the React tree, ongoing AJAX calls, etc...
  5. In render - don't add any properties to the container, so it won't be rerendered on props/state change.

Note: Before React 16.3 the standard way was to prevent rerender on props/state change by returning false in shouldComponentUpdate(), and reacting to props changes in componentWillReceiveProps(). However the latter is on it's way to being deprecated, and the former will be a recommendation instead of a strict order in the future.


This (non working) example is loosely based on the current VexFlow tutorial:

export default class ScoreComponent extends React.Component {
  constructor(props) {
    super(props);
    // 1. create a ref to a DOM element
    this.scoreElem = React.createRef();
    ...
  }

  componentDidMount() {
    const { size } = this.props;
    const elem = this.scoreElem.current;
    // 2. add a reference to the plugin's instance, so you   
    //    can call the plugin in other lifecycle methods
    this.renderer = new VF.Renderer(elem, VF.Renderer.Backends.SVG)
    renderer.resize(size.w, size.h);
    this.context = renderer.getContext();
    ...
  }  

  componentDidUpdate (prevProps) {
    // 3. if the props effect the plugin
    // do something with the plugin
    // for example:
    const { size } = this.props;
    if(size !== prevProps.size) this.renderer.resize(size.w, size.h);
  }

  componentWillUnmount() {
    // 4. teardown:
    // run VexFlow destroy method if available
    // remove non react event listeners
    // clear timeouts and intervals
    // remove DOM nodes rendered outside of react container
    // cancel ongoing AJAX calls
    // etc...
  }

  render() {
    // 5. use only ref on the returned element, any use of properties/state might rerender the element itself.
    return (
      <div className="score" ref={this.scoreElem}></div>
    );
  }

}

Upvotes: 2

Related Questions