Backbone => React - компоненты высшего порядка, наследование и специализация - PullRequest
0 голосов
/ 04 сентября 2018

У меня есть устаревшее приложение 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 считаются антишаблоном, я, естественно, сфокусировался на композиции, и в частности на компонентах высшего порядка. HOCs (которые я нахожу красивыми, но не интуитивно понятными и часто просто смущающими), кажется, включают добавление общих функций, а не специализацию / уточнение чего-то более общего. Я также рассмотрел возможность передачи более специализированных версий методов 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 голосов
/ 04 сентября 2018

IMO то, что сбивает вас с толку, это не наследование против композиции, это ваш поток данных:

Например, многие из моих производных представлений должны выполнять пользовательский рендеринг после основного рендеринга. Я использую стороннюю библиотеку 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 более типично в реагировании иметь общий класс для отображения данных и простой передачи их функциям карты в качестве подпорок, чтобы переставить / преобразовать визуализированные данные.

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

ИМО. Этот шаблон использования HOC для отвлечения трудоемкого явного использования .map в вызовах рендеринга (среди прочих случаев использования) - это шаблон, который вы ищете. Однако, как я уже говорил выше, шаблон HOC не имеет ничего общего с вашей основной проблемой общего хранилища данных между компонентами-братьями.

0 голосов
/ 06 сентября 2018

Отвечая на мой собственный вопрос, который я никогда раньше не задавал ...

Таким образом, мой вопрос действительно возник из-за беспокойства о том, что мне нужно реорганизовать большую, обязательную и основанную на состоянии кодовую базу, чтобы интегрировать ее с композиционной моделью React (также с Redux). Но после прочтения (очень проницательных и полезных) ответов на мой вопрос мне пришло в голову, что мое приложение состоит из двух параллельных частей: пользовательского интерфейса и механизма, который запускает алгоритмы (на самом деле это механизм анализа музыки). И я могу легко удалить слой Backbone View, к которому подключен движок. Итак, используя API React context , я создал «AnalysisEngineProvider», который делает механизм доступным для подкомпонентов. Движок очень императивный и классически объектно-ориентированный, и все еще использует модели Backbone, но это не имеет значения для пользовательского интерфейса, так как последний не знает своих внутренних функций - так и должно быть (модели, вероятно, будут подвергнуты рефакторингу) в какой-то момент тоже) ...

Движок также отвечает за рендеринг SVG (не с представлениями BB). Но Реакт ничего не знает об этом. Он просто видит пустой div. Я беру ref из div и передаю его движку, чтобы последний знал, где визуализировать. Кроме того, движок и пользовательский интерфейс имеют небольшой контакт - элементы div никогда не обновляются после изменений состояния React (другие компоненты пользовательского интерфейса, тем не менее, очевидно). Модели в движке только запускают обновления SVG, о которых React ничего не знает.

Я удовлетворен этим подходом, по крайней мере, на данный момент - даже если это только часть инкрементного рефакторинга в направлении решения полностью React. Это похоже на правильный дизайн приложения, какой бы фреймворк я ни использовал.

0 голосов
/ 04 сентября 2018

Как правило, если компонент не имеет состояния и не использует ловушки жизненного цикла, нет никаких причин для него быть 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 prop вместо создания getBarBaz, но это обычно не рекомендуется из-за ненужных накладных расходов.

Кроме того, значения реквизита по умолчанию можно назначать с помощью defaultProps без создания новой функции ()=>"foo" при каждом вызове компонента:

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

GeneralComponent.defaultProps = { foo: () => 'foo' };
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...