骨干=> React-高阶组件,继承和专业化

时间:2018-09-03 23:08:52

标签: reactjs inheritance backbone.js composition higher-order-components

我有一个旧版Backbone应用程序,我已经开始在React中重写它。该应用程序有一个主视图,其中包含两个子视图,这些子视图是按照排列顺序排列的。顶部面板显示一些数据,底部面板显示一些以该数据为输入的算法的结果。由于我有许多不同的数据源,每个数据源都应用了不同的算法,因此我有一个抽象的基类View,然后为每个数据源创建了子类,并根据需要添加,修饰和重写方法。像这样:

// 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);
    }
  },

})

这导致许多相似的View类的层次结构很浅。这都是非常必要的,但这是一个简单,直观且易于理解的模式,并且有效。但是,我正在努力寻找如何在React中实现相同的目标。假设React.Component的子类的子类被视为反模式,我的重点自然是组合,尤其是高阶组件。 HOC(我发现它很漂亮,但不直观,并且常常令人困惑)似乎涉及添加一般功能,而不是专门化/完善更一般的内容。我还考虑过通过props传递更专业的Componenet方法版本。但这只是意味着我必须一遍又一遍地使用相同的样板组件定义:

// 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} />
    )
  }
}

显然,以上是一个非常简单的情况,但是本质上捕获了我需要做的事情(尽管我当然会操纵状态,为简单起见,本示例不这样做)。我的意思是,我可以做这样的事情。但是我要避免不得不在整个地方重复该样板。那么,有没有更简单,更优雅的方法呢?

3 个答案:

答案 0 :(得分:2)

通常,如果组件是无状态的并且不使用生命周期挂钩,则没有理由将其设为Component类。充当名称空间且不持有状态的类可以被视为JavaScript中的反模式。

与其他框架相反,React没有模板,因此不需要映射变量以使其在视图中可用,因此唯一需要提及bar函数的地方是被称为。 JSX是JavaScript的扩展,JSX表达式可以使用当前作用域中可用的任何名称。这样就可以在没有任何类的情况下编写函数:

const getBar => "bar";
const getBaz => "baz";
const getBarBaz => getBar() + getBaz();

const MySpecialisedComponent = props => <GeneralComponent foo={getBar} />;
const MyOtherSpecialisedComponent = props => <GeneralComponent foo={getBarBaz} />;

可以将匿名函数作为foo道具来传递,而不是创建getBarBaz,但是由于不必要的开销,通常不建议这样做。

此外,可以使用defaultProps分配默认prop值,而无需在每个组件调用上创建新的()=>"foo"函数:

function GeneralComponent({ foo }) {
  return (
    <div>
      <span> {foo()} </span>
    </div>
  )
}

GeneralComponent.defaultProps = { foo: () => 'foo' };

答案 1 :(得分:1)

IMO让您失望的不是继承vs合成,而是您的数据流:

  

例如,我的许多派生视图都需要在主渲染之后进行自定义渲染。我正在使用第三方SVG库,渲染到“结果”子视图中的数据是通过分析其上方主数据视图中的渲染SVG元素得出的。

因此,您要在此处执行的操作是渲染后对远相关组件的子项更新道具,对吗?这样吗?

// 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>

有很多状态管理库可以帮助您解决此问题,即在一个组件中生成数据并将其广播到远距离相关的组件。如果您想使用内置的工具来解决此问题,则可以使用context,它为您提供了一个全局存储,您可以将其提供给想要使用它的任何组件。

在您的示例中,子类具有特定于数据的方法(add等)。 IMO在响应时更典型的做法是拥有一个通用类来显示数据,并简单地将其作为道具传递给map函数,以便重新排列/变换渲染的数据。

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,您正在寻找这种使用HOC来抽象化在渲染调用中明确使用.map的工作的模式(以及其他用途)。但是,如上所述,HOC模式与您在同级组件之间共享数据存储的主要问题无关。

答案 2 :(得分:0)

回答我自己的问题,这是我从未见过的问题...

因此,我的问题确实来自一个担忧,即我需要重构大型,命令式和有状态的代码库,以便与React的基于组合的模型(以及Redux)集成。但是,在阅读了对我的问题的回答(非常有见地和很有帮助)后,我想到我的应用程序具有两个并行部分:UI和运行算法的引擎(实际上是音乐分析引擎)。而且我可以很容易地删除引擎连接到的Backbone View层。因此,使用React的context API,我构建了一个“ AnalysisEngineProvider”,该引擎可用于子组件。该引擎非常有必要,并且经典地是面向对象的,并且仍然使用Backbone模型,但这对UI没有影响,因为UI不了解其内部-这应该是这样(模型可能会被重构出来)也在某个时候)...

引擎还负责渲染SVG(不适用于BB视图)。但是React对此一无所知。它只是看到一个空的div。我从div中选取了ref并将其传递给引擎,以便引擎知道在哪里渲染。除了引擎和UI几乎没有联系之外-div根本不会从React状态更改中进行更新(显然,UI的其他组件也是如此)。引擎中的模型只会触发SVG的更新,而React对此一无所知。

至少到现在,我对这种方法感到满意-即使这只是对完全React解决方案的增量重构的一部分。无论我碰巧正在使用什么框架,这都感觉像是针对该应用程序的正确设计。