Search code examples
reactjsinheritancebackbone.jscompositionhigher-order-components

Backbone => React - Higher Order Components, inheritance and specialisation


I have a legacy Backbone app which I have begun to rewrite in React. The app has a main view containing two subviews, arranged vetically. The top panel displays some data, and the bottom one displays the result of some algorithm taking this data as input. Since I have many different data sources, each with a different algorithm applied to it, I have an abstract base View class, which I then subclass for each data source, adding, decorating and overriding methods as necessary. Somewhat like this:

// Base View.
const BaseView = Backbone.View.extend({

  events: {},

  initialize() {
    this.subViewA = // instantiate subview...
    this.subViewB = // instantiate subview...
  },

  generateResultData() {
    // 'Abstract' method which should be specialised to generate data rendered by subViewB...
  },

  render() {
    // render subviews...
  },

});

// Derived View.
const Derived = BaseView.extend({

  events: {
    // event handlers...
  },

  add(a, b) {
    return a+b;
  },

  // additional methods...

  generateResultData() {
    return {
      result: this.add(2,2);
    }
  },

})

This results in a shallow hierarchy of many similar View classes. It's all terribly imperative, but it's a simple, intuitive and easy-to-reason-about pattern, and just works. I'm struggling to see how to achieve the same thing in React, however. Given that subclassing of subclasses of React.Component is considered an anti-pattern, my focus has naturally been on composition, and in particular Higher Order Components. HOCs (which I find beautiful, but unintuitive and often just downright confusing) seem to involve adding general features, rather than specialising/refining something more general. I have also considered passing in more specialised versions of Componenet methods through props. but that just means I have to use the same boilerplate Component definition over and over again:

// General functional component, renders the result of prop function 'foo'.
function GeneralComponent(props) {
  const foo = this.props.foo || ()=>"foo";
  return (
    <div>
      <span> { this.props.foo() } </span>
    </div>
  )
}

// Specialised component 1, overrides 'foo'.
class MySpecialisedComponent extends React.Component {
  foo() {
    return this.bar()
  }

  bar() {
    return "bar"
  }

  render() {
    return (
      <GeneralComponent foo={this.foo} />
    )
  }
}

// Specialised component 2, overrides 'foo' and adds another method.
class MyOtherSpecialisedComponent extends React.Component {
  foo() {
    return this.bar() + this.bar()
  }

  bar() {
    return "bar"
  }

  baz() {
    return "baz"
  }

  render() {
    return (
      <GeneralComponent foo={this.foo} />
    )
  }
}

The above is a very simplistic case, obviously, but essentially captures what I need to do (though I would of course be manipulating state, which the example does not do, for simplicity). I mean, I could just do things like that. But I want to avoid having to repeat that boilerplate all over the place. So is there a simpler and more elegant way of doing this?


Solution

  • IMO what is throwing you off isn't inheritance vs composition, it's your data flow:

    For example, many of my derived views need to do custom rendering after the main render. I'm using a third-party SVG library, and the data rendered into the 'result' subview is derived from analysis of rendered SVG elements in the main data view above it

    So what you're trying to do here is have a child update props of a distantly related component after render, correct? Like this?

    // after the svg renders, parse it to get data
    <div id="svg-container">
      <svg data="foo" />
      <svg data="bar />
    </div>
    
    // show parsed data from svg after you put it through your algos
    <div id="result-container">
      // data...
    </div>
    

    There's a lot of state management libraries out there that will help you with this problem, that is, generating data in one component and broadcasting it to a distantly related component. If you want to use a tool built-in to react to address this you may want to use context, which gives you a global store that you can provide to any component that wants to consume it.

    In your example your child classes have data-specific methods (add, etc.). IMO it's more typical in react to have a generic class for displaying data and simply passing it down map functions as props in order to rearrange/transform the rendered data.

    class AbstractDataMap extends PureComponent {
      static defaultProps = {
        data: [],
        map: (obj, i) => (<div key={i}>{obj}</div>)
      };
    
      render() {
        const { data, map, children } = this.props;
        const mapped = data.map(map);
    
        return (
          <Fragment>
            {mapped.map((obj, i) => (
              children(obj, i)
            ))}
          </Fragment>
        );
      }
    }
    
    // in some other container
    class View extends Component {
      render() {
        return (
          <div>
            <AbstractDataMap data={[1, 2, 3]} map={(n) => ({ a: n, b: n + 1 })}>
              {({ a, b }, i) => (<div key={i}>a: {a}, b: {b}</div>)}
            </AbstractDataMap>
    
            <AbstractDataMap data={[2, 4, 6]} map={(n) => (Math.pow(n, 2))}>
              {(squared, i) => (<div key={i}>squared: {squared}</div>)}
            </AbstractDataMap>
          </div>
        );
      }
    }
    

    IMO this pattern of using an HOC to abstract away the labor of explicitly using .map in your render calls (among other uses) is the pattern you are looking for. However, as I stated above, the HOC pattern has nothing to do your main issue of shared data store across sibling components.