[Разработка веб-сайтов, JavaScript, Проектирование и рефакторинг, ReactJS] Вариант повторного использования кода компонентов, который упустили в веб-разработке

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
10-Мар-2021 13:31

В этом статье я покажу, как для React-компонентов реализовать один из подходов на основе сущностей и их составляющих. Как я упоминал в предыдущей статье "Техники повторного использования кода" в главе "Entity Component (EC)", я не знаю точное название подхода. В статье я буду называть его Entity Component (EC). Entity Component используется для решения той же проблемы, что HOC и Custom hooks – повторно использовать код между множеством однотипных объектов/функций и разбить сложный объект на более простые составляющие. Эта необходимость появилась довольно давно, гораздо раньше, чем с ней столкнулись в вебе. И давно были придуманы эффективные решения.Custom hooks и Entity Component - оба добавляют к объекту некий функционал. Тем самым они близки к паттерну «стратегия». Но, Entity Component – решение для объектов (судя по тому, что я встречал), а Custom hooks - для функций. Даже если этот подход вам не интересен, как минимум вы увидите, как можно переделать структуру функциональных компоненты и компонентов-классов под свои нужды. Узнаете нестандартные приемы, которые можно использовать при разработке компонентов.
Те, кто пишет компоненты-классы, узнает, как повторно использовать код более эффективным способом, чем HOC, не создавая лишние компоненты-обертки.Если разобраться, то подход довольно прост. Но, возможно, я не смогу его достаточно понятно объяснить. Он давно используется в геймдеве, в том числе в разработке UI. Здесь многие возразят: "это же подход из другой области и вряд ли будет хорош в вебе". Это так, если окружение и разработка сильно отличаются. Здесь же они похожи. На странице и в игровой сцене используется дерево объектов (DOM и дерево объектов сцены). Компоненты в вебе состоят из вложенных объектов и объекты в играх тоже состоят из вложенных объектов. Компоненты в вебе могут состоять из составляющих (атрибуты/директивы или хуки), и объекты в играх тоже состоят из составляющих. Отличия есть, но не такие, чтобы нельзя было применять общие подходы. Просто их нужно адаптировать под используемую область.Исходники и примеры компонентов, созданных описываемым подходом, доступны по ссылкам ниже.Код из статьи: github
Более полная реализация: github, codesanboxСодержание
Недостатки React-компонентов и хуков. Преимущества и недостатки моей реализации Entity ComponentПреимущества компонентов и хуков я не буду расписывать. Их не сложно найти. Про недостатки скажу, что они не заметны в небольших компонентах и далеко не всегда ведут к проблемам.Недостатки компонентов (на мой взгляд):
  • Логика компонента объединены с View. Когда-то view слоем была вся клиентская часть. Сейчас к View слою обычно относят компонент с его логикой. В будущем View слоем будет часть компонента без логики. В Vue и Angular View часть уже отделена от компонента, что дает больше гибкости.
  • Слишком большая ответственность (нарушение первого принципа SOLID). Компонент является как-бы контейнером для функций с логикой (custom hooks), содержит реализацию View, содержит свою логику, обрабатывает события жизненного цикла. Это очень странно, учитывая популярность Redux, где много бойлерплейта и пропагандируется чистота функций в редьюсерах. В случае функциональных компонентов все в точности да наоборот - грязные функции-компоненты и маленький размер компонентов.
  • У компонентов нет четкой зоны ответственности. Программист всегда стоит перед выбором, где лучше написать код – в пользовательском хуке или в компоненте. Плохо, когда один и тот же код используется в сущностях, у которых разное назначение. У кода любого должно быть свое определенное место в программе.
Недостатки хуков (на мой взгляд):
  • При необходимости расширения, придеться изменять код внутри компонента или хука. Это нарушение второго принципа SOLID. К каким последствиям это может привести? Допустим вы полгода пользовались сторонним сложным компонентом. Затем от заказчика пришло требование, которое нельзя реализовать с помощью этого компонента. Хорошо, если у автора компонента все разбито на независимые хуки, и он предоставил возможность из них собрать свой. Если нет, то придется или писать свой компонент с нуля, или же делать форк. В теории же можно было бы реализовать в хуках возможность их удаления из компонента или добавление.
  • Код в функциональных компонентах выполняется при каждом рендеринге. Это не приводит к серьезным проблемам производительности, но приводит к ошибкам и усложнению кода. Например, в случае useState(), надо следить, чтобы кто-нибудь случайно не передавал props useState(props.value). Или же могут быть лишние перерисовки, если в массивы зависимостей компонента передаются не мемоизированные функции и данные. В книге Крокфорд Дугла «Как устроен JavaScript [2019]» в главе «Как работают генераторы» я столкнулся с такой фразой: «В Structured Revolution утверждалось, что потоки управления должны быть абсолютно понятными и предсказуемыми.» Другими словами, хороший код – как можно более предсказуемый и понятный. Любой хук же надо читать как условный код: If (isFirstRender) { ... } else { ...}. А ведь можно было бы в компоненте-объекте сделать что-то вроде addEffect(callback, dependencies) и избежать подобной ситуации.
Моя реализация Entity Component имеет несколько преимуществ по сравнению с custom hooks и нынешними react-компонентами:
  • Можно добавлять и удалять логику в существующем компоненте, чего не позволяют custom hooks. Изменять логику компонента можно делать даже в запущенном приложении.
  • View отделен от компонента, что дает дополнительную гибкость.
  • Объекты с логикой отделены от компонента и избегается написание пользовательской логики в самом компоненте.
  • Нет необходимости использовать что-то вроде useRef, useCallback, т.к. в отличие от функций, в объектах переменные и функции можно легко привязать к объекту обычным присваиванием.
Недостатки моей реализации Entity Component:
  • Производительность. Я не занимался оптимизацией. Если оптимизировать мой код, то по скорости он скорее всего будет близок к компонентам-классам. Большую производительность можно было бы получить, если бы подход был бы реализован в самом React.
  • Больше кода, чем в функциональных компонентах. Примерно, как в компонентах-классах.
  • Возможно не очень удобная реализация. Для повышения удобства потребуется потратить больше времени. Т.к. решение не встроено в фреймворк, а сделано поверх существующих типов компонентов, компонент разделен на большее число составляющих, чем нужно. К тому же я делал так, чтобы как можно больше кода работало с обоими типами компонентов (с компонентами-классами и с функциональными).
  • Кому-то не понравится возврат к методам жизненного цикла вместо использования эффектов. Это опционально. Можно реализовать аналоги эффектов и других хуков, хотя, так же кратко может не получиться. С точки зрения разработки, эффекты - это другая парадигма. Но, с точки зрения реализации - это просто синтаксический сахар над событиями жизненного цикла. Да и использование методов жизненного цикла - это не плохой подход, а просто другой.
Основные программные сущности в моей реализации Entity Component
  • React-компонент – просто содержит ссылку на объект-контейнер и используется для его инициализации. Также используется react-ом для рендеринга. Нужен только потому, что это неотъемленная часть React.
  • Container – объект без пользовательской логики, который содержит объекты behaviours с пользовательской логикой.
  • Behaviour (поведение) – объект с логикой или частью логики компонента. Что-то вроде пользовательских хуков, но является объектом. В компоненте может быть несколько таких объектов, каждый из которых реализует определенный функционал.
  • render – просто функция с JSX кодом. Не компонент! Может быть объявлена вне компонента и использоваться в нескольких разных компонентах. Через параметры получает данные и функции, которые использует в своем теле.
  • Config – объект с параметрами, по которым на момент создания определяется функционал компонента. Задается в компоненте, но может быть вынесен отдельно. В нем указывается behaviours и render-функция, которые использует данный компонент, а также другие опции.
  • Event emitter – используется для проброса событий жизненного цикла компонента в объекты с логикой (behaviours).
 Важные нюансы реализации:
  • Хоть это и не обязательно, но для более читабельного кода используются классы. Классы-наследники создаются относительно медленно, но пока не будет создаваться хотя бы несколько тысяч экземпляров классов, падение производительности не будет заметным даже на мобильных.
  • Вместо конструктора в классах используется init. Это нужно, чтобы можно было использовать this до вызова родительского конструктора super(). Иногда это необходимо.
  • Стрелочные функции не используются при объявлении методов в базовых классах. Если их использовать, тогда нельзя будет переопределить this для этого метода в наследнике, и он всегда будет указывать на родительский класс.
  • field – с помощью "_" указывается protected свойство или метод.
Большая часть кода применима к обоим типам компонентов – к функциональным и к компонентам-классам. Отличающиеся места будут описаны по ходу.За идеи хранения нужного функционала в useRef и проброса событий из хуков – спасибо @Alexandroppolus! Его коммент мне очень помог в реализации версии на функциональных компонентах.Изменяем react-компоненты. Часть 1: Event emitterТ.к. логика будет находиться не в компоненте, а в других объектах, то нужно как-то пробрасывать в них события жизненного цикла компонента. Этим будет заниматься класс EventEmitter. Имя события в нем - это то же самое, что имя метода жизненного цикла в behaviours.В моем репозитории есть примеры двух реализаций эмиттеров событий. Здесь же пример одного из них, который попроще. В примере 2 простых метода:
1) callMethodInBehaviour – вызывает метод в behaviour, имя которого совпадает с именем события.
2) callMethodInAllBehaviours – вызывает метод во всех behaviours компонента.EventEmitter
export class SimpleEventEmitter {
  _behaviourArray;
  init(behaviourArray) {
    this._behaviourArray = behaviourArray;
  }
  callMethodInBehaviour(methodName, behaviourInstance, args = []) {
    const behaviourMethod = behaviourInstance[methodName];
    if (behaviourMethod) {
      behaviourMethod.apply(behaviourInstance, args);
    }
  }
  callMethodInAllBehaviours(methodName, args = []) {
    this._behaviourArray.forEach(beh => {
      if (beh[methodName]) {
        beh[methodName](...args);
      }
    });
  }
}
Далее задан перечень событий жизненного цикла. Добавлены новые события, вызываемые при удалении и добавлении behaviour, т.к. те могут быть добавлены или удалены во время существования компонента.LifeCycleEvents
export const LifeCycleEvents = {
  BEHAVIOUR_ADDED: 'behaviourAdded',
  COMPONENT_INITIALIZED: 'componentInitialized',
  COMPONENT_DID_MOUNT: 'componentDidMount',
  COMPONENT_DID_UPDATE: 'componentDidUpdate',
  // для вызова из useEfect()
  COMPONENT_DID_UPDATE_EFFECT: 'componentDidUpdateEffect',
  BEHAVIOUR_WILL_REMOVED: 'behaviourWillRemoved',
  COMPONENT_WILL_UNMOUNT: 'componentWillUnmount',
  // остальные события не реализованы в примерах
};
Изменяем react-компоненты. Часть 2: создание объекта-контейнераОбъект контейнер используется для:
  • хранения всех behaviours компонента и для доступа к ним
  • хранения объекта-словаря состояний для всех его behaviours
  • создания и удаления behaviours из своих списков
  • хранения объекта config
  • хранения эмиттера событий
  • получения данных из behaviours
  • вызова функции render из config, передачи в нее данных из behaviours, затем возврата получившегося JSX кода в компонент.
В методе render контейнера вызывается метод mapToMixedRenderData. В данном примере этот метод вызывает в каждом behaviour метод mapToRenderData и смешивает все возвращаемые данные в один объект. Это похоже на optionMergeStrategiesв Vue. Подобные стратегии для решения конфликтов пересечения имен у меня реализованы в другой ветке. В коде к статье эта часть убрана.Пример абстрактного базового класса для контейнеров: AbstractContainer
import { LifeCycleEvents } from './LifeCycleEvents';
import { SimpleEventEmitter } from './eventEmitters/SimpleEventEmitter';
export class AbstractContainer {
  _eventEmitter;
  _config;
  // Array with all behaviours of component
  behaviourArray = [];
  // Object (dictionary) with all behaviours of container.
  // To simplify access to behaviour by name
  behs = {};
  // Object (dictionary) with pairs [behaviourName]: behParamsObject
  behsParams = {};
  get eventEmitter() {
    return this._eventEmitter;
  }
  get config() {
    return this._config;
  }
  get state() {
    console.error('container state getter is not implemented');
  }
  get props() {
    console.error('container props getter is not implemented');
  }
  init(config, props) {
    this._eventEmitter = new SimpleEventEmitter();
    this._eventEmitter.init(this.behaviourArray);
    this._config = config;
    this._createBehaviours(props);
  }
  _createBehaviours(props) {
    const defaultBehaviours = props?.defaultBehaviours;
    const allBehParams = defaultBehaviours
      || this.config.behaviours || [];
    // create behaviours
    allBehParams.forEach(oneBehParams => {
      const { behaviour, initData, ...passedBehParams } = oneBehParams;
      this.addBehaviour(behaviour, props, initData, passedBehParams);
    });
    this._eventEmitter.callMethodInAllBehaviours(
      LifeCycleEvents.COMPONENT_INITIALIZED,
      [props],
    );
  }
  setState(stateOrUpdater){
    console.error('container setState is not implemented');
  }
  addBehaviour(behaviour, props, initData, behaviourParams = {}) {
    const newBeh = new behaviour();
    this.behaviourArray.push(newBeh);
    this.behs[ newBeh.name ] = newBeh;
    this.behsParams[ newBeh.name ] = behaviourParams;
    if (newBeh.init) {
      newBeh.init(this, props, initData, behaviourParams);
    }
    this._eventEmitter.callMethodInBehaviour(
      LifeCycleEvents.BEHAVIOUR_ADDED, newBeh);
    return newBeh;
  }
  removeBehaviour(behaviourInstance) {
    const foundIndex = this.behaviourArray.indexOf(behaviourInstance);
    if (foundIndex > -1) {
      this._eventEmitter.callMethodInBehaviour(
        LifeCycleEvents.BEHAVIOUR_WILL_REMOVED, behaviourInstance);
      this.behaviourArray.splice(foundIndex, 1);
      delete this.behs[behaviourInstance.name];
      delete this.behsParams[behaviourInstance.name];
    } else {
      console.warn(
        `removeBehaviour error: ${behaviourInstance.name} not found`
      );
    }
  }
  // Return all behaviours renderData mixed in single object.
  _mapToMixedRenderData() {
    let retRenderData = this.behaviourArray.reduce((mixedData, beh) => {
      const behRenderData = beh.mapToRenderData();
      Object.assign(mixedData, behRenderData);
      return mixedData;
    }, {});
    return retRenderData;
  }
  render() {
    const renderFunc = this.config.render
      ? this.config.render
      : ({ props }) => props?.children;
    return renderFunc({
      props: this.props,
      ...this._mapToMixedRenderData(this)
    });
  };
}
И конкретные классы для компонентов-классов и для функциональных компонентов. Они не сильно отличаются. Контейнер для компонентов-классов обращается к компоненту при использовании props, state, setState. Контейнер для функциональных компонентов сам хранит полученные от компонента props, а также state и setState, получаемые извне от хука useState.ContainerForClassComponent и ContainerForFunctionalComponent
import { AbstractContainer } from '../AbstractContainer';
export class ContainerForClassComponent extends AbstractContainer {
  _component;
  get state() {
    return this._component.state;
  }
  get props() {
    return this._component.props;
  }
  setState = (stateOrUpdater) =>{
    this._component.setState(stateOrUpdater);
  };
  init(config, props, component) {
    this._component = component;
    super.init(config, props);
  }
}
import { AbstractContainer } from '../AbstractContainer';
export class ContainerForFunctionalComponent
  extends AbstractContainer {
  _props;
  _state;
  init(config, props, state, setState) {
    this._props = props;
    this._state = state;
    this.setState = setState;
    super.init(config, props);
  }
  get state() {
    return this._state;
  }
  set state(state) {
     this._state = state;
  }
  get props() {
    return this._props;
  }
  set props(props) {
    this._props = props;
  }
}
Изменяем react-компоненты. Часть 3: связывание компонента с логикой контейнераКлассы контейнеры созданы. Осталось использовать их в компонентах.Для компонентов-классов создадим класс ComponentWithContainer, в котором инициализируем контейнер и в методах жизненного цикла компонента вызываем соответствующие методы в behaviours с помощью event emitter-а.Чтобы при создании компонентов-классов писать меньше кода, создание класса обернуто в функцию createComponentWithContainer.
Таким образом, при создании компонентов больше не нужно создавать классы-наследники. Для всех компонентов будет используется один общий класс - ComponentWithContainer. Компоненты будут отличаться только передаваемым набором параметров в config.ComponentWithContainer
import { LifeCycleEvents } from '../LifeCycleEvents';
import { ContainerForClassComponent } from './ContainerForClassComponent';
class ComponentWithContainer extends React.Component {
  _container;
  constructor(props, context, config) {
    super(props, context);
    this._container = new ContainerForClassComponent();
    this._container.init(config, props, this);
  }
  componentDidMount() {
    this._container.eventEmitter.callMethodInAllBehaviours(
      LifeCycleEvents.COMPONENT_DID_MOUNT,
    );
  }
  componentDidUpdate(...args) {
    this._container.eventEmitter.callMethodInAllBehaviours(
      LifeCycleEvents.COMPONENT_DID_UPDATE, args,
    );
  }
  componentWillUnmount() {
    this._container.eventEmitter.callMethodInAllBehaviours(
      LifeCycleEvents.BEHAVIOUR_WILL_REMOVED,
    );
    this._container.eventEmitter.callMethodInAllBehaviours(
      LifeCycleEvents.COMPONENT_WILL_UNMOUNT,
    );
  }
  render() {
    return this._container.render();
  }
}
export const createComponentWithContainer = (componentName, config) => {
  return class extends ComponentWithContainer {
    constructor(props, context) {
      super(props, context, config);
    }
    static displayName = componentName;
  };
};
Теперь аналог для функциональных компонентов.
Чтобы где-то хранить контейнер, понадобиться хук useRef.
А чтобы хранить общее состояние в виде словаря для behaviours и изменять его, понадобиться хук useState.Для проброса событий жизненного цикла компонента, понадобится хук useEffect. На самом деле понадобиться еще useLayout для более корректного проброса событий, но для краткости я пропущу этот момент. В отдельной ветке в репозитории используются оба хука.useBehaviours
import { useRef, useState, useEffect } from 'react';
import { LifeCycleEvents } from '../LifeCycleEvents';
import { ContainerForFunctionalComponent }
  from './ContainerForFunctionalComponent';
export const useBehaviours = (config = {behaviours: []}, props) =>{
  let isFirstRender = false;
  const ref = useRef();
  // create shared state
  const [state, setState] = useState({});
  // get exist or get new passed initial config
  const initialConfig = ref.current
    ? ref.current.config
    : config;
  if (!ref.current) {
    ref.current = new ContainerForFunctionalComponent();
    ref.current.init(initialConfig, props, state, setState);
    isFirstRender = true;
  } else {
    // update state and props in container
    ref.current.state = state;
    ref.current.props = props;
  }
  const container = ref.current;
  callLifeCycleEvents(
    container.eventEmitter, initialConfig, isFirstRender);
  return container.render();
};
const callLifeCycleEvents =
  (eventEmitter, initialConfig, isFirstRender) => {
  // on mount, unmount
  useEffect(() => {
    eventEmitter.callMethodInAllBehaviours(
      LifeCycleEvents.COMPONENT_DID_MOUNT);
    return () => {
      eventEmitter.callMethodInAllBehaviours(
        LifeCycleEvents.BEHAVIOUR_WILL_REMOVED);
      eventEmitter.callMethodInAllBehaviours(
        LifeCycleEvents.COMPONENT_WILL_UNMOUNT);
    }
  }, []);
  // on update
  useEffect(() => {
    if (!isFirstRender) {
      eventEmitter.callMethodInAllBehaviours(
        LifeCycleEvents.COMPONENT_DID_UPDATE_EFFECT);
    }
  });
};
Изменяем react-компоненты. Часть 4: создание базового объекта (behaviour) для переиспользования логики в компонентахКак я уже писал, для написания пользовательской логики и для ее повторного использования в других компонентов, используется специальный объект – behaviour.
Ниже пример базового класса для создания таких объектов.BaseBehaviour
import lowerFirst from "lodash/lowerFirst";
export class BaseBehaviour {
  // необязательное поле на тот случай, если понадобиться сравнивать
  // по типу.
  type = Object.getPrototypeOf(this).constructor.name;
  // используется как идентификатор
  name = lowerFirst(this.type);
  // Данные и функции, которые передается в компонент через функцию
  // mapToRenderData. Используется когда нужно, чтобы при каждом
  // рендеринге не создавались новые объекты и функции, а также для
  // их передачи в props дочерних компонентов. Таких образом, это может
  // помочь избежать лишних перерисовок. useCallback в таком случае
  // становится ненужным.
  passedToRender = {};
  init(container, props, initData = {}, config) {
    this.container = container;
    if (initData.defaultState) {
      this.defaultState = initData.defaultState;
    }
  }
  // об этом позже
  get ownProps() {
    const propBehaviourName = `bh-${this.name}`;
    return this.container.props?.[propBehaviourName];
  }
  // Эмуляция собственного состояния для каждого behaviour.
  // На самом деле используется объект-словарь, хранящийся в контейнере
  // или в компоненте. Каждое поле в объекте-словаре указывает
  // на состояние в одном из behaviour. Состояние behaviour передается
  // в компонент с помощью метода mapToRenderData.
  get state() {
    const defaultValue = this.defaultState;
    return this.container.state
      ? this.container.state[this.name] || defaultValue
      : defaultValue;
  }
  // Изменяет состояние behaviour и вызывает обновление компонента.
  // Cигнатура этого метода эквивалентна методу setState
  // компонента-класса.
  setState(stateOrUpdater, callback) {
    if (typeof stateOrUpdater === 'function') {
      const updater = stateOrUpdater;
      this.container.setState((prevState) => {
          return {
            ...prevState,
            [ this.name ]: updater(prevState[ this.name ])
          };
        },
        callback);
      return;
    }
    const newPartialState = stateOrUpdater;
    this.container.setState((prevState) => {
      return {
        ...prevState,
        [this.name]: newPartialState
      };
    });
  }
  // Возвращает данные и функции, которые в итоге передадуться
  // в функцию render, указанную в config компоненте
  mapToRenderData() {
    return {
      ...this.state,
      ...this.passedToRender,
    };
  }
  // Очистка состояния при удалении
  behaviourWillRemoved() {
    this.setState(undefined);
  }
}
Изменяем react-компоненты. Часть 5: Примеры создания компонентов. Итоговая схемаРеализация подхода Entity Component закончена. Теперь можно создавать компоненты. Рассмотрим создание компонента-класса и функционального компонента на примере простого счетчика. Больше примеров использования можно найти в репозитории, ссылки на который указаны в начале статьи.Примеры компонентов и behaviour
import { createComponentWithContainer }
  from "../core/forClassComponent/createComponentWithContainer";
import { BaseBehaviour } from "../core/BaseBehaviour";
// Здесь пишется логика компонента и описываются данные, используемые
// в функции render
class CounterBehaviour extends BaseBehaviour {
  defaultState = { count: 0 };
  passedToRender = {
    setCount: value => {
      this.setState({ count: value });
    }
  };
}
export const CounterExample = createComponentWithContainer(
  'CounterExample', {
    behaviours: [{ behaviour: CounterBehaviour }],
    render: ({ count, setCount }) => (
      <>
        <h3>Counter Example</h3>
        <p>You clicked {count} times</p>
        <button onClick={() => setCount(count + 1)}>Click me</button>
      </>
    ),
  });
export const CounterExampleWithHooks = (props) => {
  return useBehaviours({
      behaviours: [{ behaviour: CounterBehaviour }],
      render: ({ count, setCount }) => (
        <>
          <h3>Counter Example</h3>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>Click me</button>
        </>
      ),
    },
    props);
};
Как видно из примера, behaviours могут быть использованы в обоих типах компонентов. Большинство часто используемых методов жизненного цикла имеют одинаковые имена. Одинаковые функцию render и объект config можно вынести вне компонента и использовать в похожих компонентах.Т.к. в behaviour метод mapToRenderData вызывается при каждом рендере компонента, в нем можно использовать хуки. Это может помочь сэкономить время, если уже есть много готовых хуков. Также это позволит получить другие преимущества хуков. Пример использования хуков, а также всех методов жизненного цикла есть в \src\LifeCycleExampleWithHooks\ LifeCycleExampleWithHooks.js
Но, я рекомендую не усложнять и не использовать несколько подходов в одном проекте.Для разработчика потоки данных в приведенных составляющих компонента выглядят примерно, как на схеме ниже. Синим отмечен поток при вызове методов жизненного цикла и создании компонента. Зеленым отмечен поток возвращаемых данных при рендеринге компонента. Разработчику практически нет необходимости взаимодействовать с контейнером вручную.
  • Сначала компонент получает props.
  • behaviour при необходимости берет props из компонента, либо получает их при вызове методов жизненного цикла.
  • При рендере компонента вызывается функция render из config, затем вызывается метод mapToRenderData в behaviour. mapToRenderData считывает объекты state и passedToRender, объединяет их в объект renderData и передает дальше.
  • Далее функция render в config-е получает объект renderData и использует его в JSX коде.
  • Далее в компонент возвращается готовый JSX код.
Дополнение. Группировка propsВ BaseBehaviour вы уже видели геттер ownProps:
get ownProps() {
  const propBehaviourName = `bh-${this.name}`;
  return this.container.props?.[propBehaviourName];
}
Пришло время рассказать для чего он нужен. Он позволяет задавать props, которые нужны только текущему bahaviour. Например:
<Form
  bh-bindModel={customModel}
  bh-formController={{onSubmit: customAction }}
/>
Через префикс "bh-" указаны имена 2-х  behaviours и объект с данными, которые нужны только им. Меня мотивировала создать этот геттер работа с фреймворком React-Admin. В некоторых его компонентах очень много props и сложно разобраться, к какому компоненту/хуку/HOC они относятся. Пример такого компонента: https://github.com/marmelab/react-admin/blob/master/packages/ra-ui-materialui/src/form/SimpleForm.tsxДополнение. Директивы - это не то, за что их принимают. Так почему бы и нет?Грубо говоря, директивы в Angular и в Vue являются дополнительной программной сущностью для повторного использования кода в компонентах. Очень здорово, но я считаю ненужным усложнением вводить дополнительную сущность для этого. Для повторного использования кода достаточно одного вида сущностей.Директива в моем понимании – это средство расширения функционала компонента через атрибут (props в react-компоненте). Директивы в моем примере – это просто средство задания behaviours компонента через props. В React решили отказаться от директив. Скорее всего у авторов React не такое виденье директив, как у меня. Атрибуты - это неотъемлемая часть HTML. Вполне естественно использовать их для задания поведения компонента. Да, можно без них. Но тогда вместо создания 5 типов компонентов и 5 типов директив может потребоваться создание 50-ти типов компонентов.
Вернемся к моему коду. В методе контейнера вы уже видели следующий код:
_createBehaviours(props) {
  const defaultBehaviours = props?.defaultBehaviours;
  const allBehParams = defaultBehaviours || this.config.behaviours || [];
То есть behaviours компонента можно задать через свойство defaultBehaviours. Это позволяет переопеределить behaviours, заданные при объявлении компонента. Это позволяет в разных частях проекта использовать один и тот же тип компонента, но с разным поведением.CounterBehaviour из примера выше можно задать так, а не в коде самого компонента:
<CounterExample defaultBehaviours={[
    {behaviour: CounterBehaviour, initData: {count: 0}
  ]}
/>
Заключение. В каких проектах Entity Component может быть полезенЯ особо не планирую развивать данный проект. Если кто-то с хорошим опытом хочет заняться развитием проекта, пишите в личку. Как разработчику, использовавшему подход Entity Component в другом стеке и оценившему его преимущества, мне было интересно реализовать его для объектов со схожей структурой (компонент с логикой и с вложенными компонентами) и продемонстрировать остальным.Я не думаю, что этот подход станет популярным в веб-разработке. По крайней мере, не в ближайшем будущем. Для продвижения потребовалось бы потратить много сил и времени. Решение не совсем завершено и сделано только в целях демонстрации подхода. Хотя, и этого вполне достаточно для использования в проектах. Этот подход может пригодиться в больших компаниях, где не принято использовать сторонние компоненты, а где разрабатывают собственную библиотеку компонентов, которую используют другие команды. EC гибче популярных в вебе подходов, т.к. он направлен не на создание компонентов, а на уровень ниже - на создании составляющих компонентов.
Также данный подход будет полезен, если вы создаете компоненты под разные платформы (React Native и Web) и вам нужно немного разное поведение одного компонента и немного разный JSX код. Я уже писал в одном комментарии такой пример:
const renderButtonWeb = ({ propA, propB, ...props}) => (<div> ... </div>);
const renderButtonNative = ({ propA, propB, ...props}) => (<div> ... </div>);
const WebButton = createContainerComponent("WebButton", {
  behaviours: [
     { behaviour: CommonButtonMethods },
     { behaviour: WebButtonMethods }
  ],
  render: renderButtonWeb
});
const NativeButton = createContainerComponent("NativeButton", {
   behaviours: [
     { behaviour: CommonButtonMethods },
     { behaviour: NativeButtonMethods }
   ],
   render: renderButtonNative
});

===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_proektirovanie_i_refaktoring (Проектирование и рефакторинг), #_reactjs, #_react, #_javascript, #_proektirovanie (проектирование), #_razrabotka_vebsajtov (
Разработка веб-сайтов
)
, #_javascript, #_proektirovanie_i_refaktoring (
Проектирование и рефакторинг
)
, #_reactjs
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 19:35
Часовой пояс: UTC + 5