В этом статье я покажу, как для 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.
-
Основные программные сущности в моей реализации Entity Component
-
Изменяем react-компоненты. Часть 2: создание объекта-контейнера.
-
Изменяем react-компоненты. Часть 3: связывание компонента с логикой контейнера.
-
Изменяем react-компоненты. Часть 5: Примеры создания компонентов. Итоговая схема.
-
Дополнение. Директивы — это не то, за что их принимают. Так почему бы и нет?
-
Заключение. В каких проектах Entity Component может быть полезен.
Недостатки 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 });
ссылка на оригинал статьи https://habr.com/ru/post/545064/
Добавить комментарий