От переводчика: Я прототипирую интерфейсы на Ember.js. И всегда испытываю трудности, когда нужно переключиться на React и написать что-нибудь используя этот de facto выбор по-умолчанию в современной фронт-энд разработке. Каждый раз сталкиваешься с трудностями на ровном месте и вынужден думать о том, о чем привык не думать, писать велосипеды. Мне всегда хотелось это как-то выразить и на днях я нашел статью, где автор очень наглядно показывает разницу на кодовых примерах. Представляю вашему вниманию ее перевод.

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

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

Дисклеймер

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

Примеры кода React, показанные в этом посте, по большей части используют React.Component. Однако основная идея не сильно поменяется, если вы будете использовать функциональные компоненты. Чтобы быть последовательным, я решил использовать только один вариант реализации компонентов React из найденного мной сообщения в блоге о шаблонах . Пожалуйста, обратитесь к самому посту, чтобы увидеть примеры реализации с функциональными компонентами.

Рассмотренные паттерны

В приведенном выше сообщении с паттернами для React-компонентов представлены три варианта, которые предназначены для «помощи в решении проблем, возникающих в больших React-приложениях».

Предложены следующие паттерны:

  1. Compound Components

  2. Flexible Compound Components

  3. Provider Pattern

Я рассмотрю идеи, лежащие в основе этих техник, и попытаюсь реализовать их в Ember.js.

Паттерн Compound Component

Использование паттерна в React

Данный паттерн используется, если вы хотите создать абстракцию компонента, в которой родительский элемент передает свое состояние дочерним. В посте о шаблонах компонентов React представлен компонент, похожий на select-элемент в HTML, в качестве примера, где у вас есть верхний уровень, в этомselect затем используется несколько option-элементов для создания более сложного взаимодействия.

В приведенном примере мы реализуем <RadioImageForm>-компонент, который управляет состоянием выбираемого значения, где потребители компонента могут настраивать отображаемый пользовательский интерфейс через  компонент <RadioImageForm.RadioInput>, который отображает радио-переключатель с изображением. Пользователь может изменить значение, кликнув по соответствующему изображению.

(прямая ссылка на codesandbox)

Проблема, которую мы хотим решить, заключается в том, что мы хотим предоставить API, который упростит использование <RadioImageForm> без изучения его внутренней реализации.

Для этого нам нужно передать состояние текущего выбранного значения и функцию, которую мы хотим запускать для родительского объекта, когда пользователь щелкает по<RadioImageForm.RadioInput>.

В React вы можете добиться этого, реализовав метод рендеринга, который выглядит следующим образом:

Код RadioImageForm.render() и RadioImageForm.RadioInput

В этом примере мы проходим по всем дочерним элементам, а затем на каждом применяем React.cloneElement, передавая дополнительные свойства, которые не были указаны в клиентском коде.

Мы также создаем статическое свойство RadioInput, которое генерирует RadioInput после того как потребитель передает ему label,  valuename, imageSrc и key.

Чтобы использовать наш компонент в коде приложения, мы напишем следующий код:

Этот же паттерн в Ember.js

Повторить реализацию Compount Component в Ember не представляет сложности. Мы создаем родительский компонент, который реализует onChangeи содержит currentValue. Чтобы дать потребителю возможность работать с RadioInput, мы включаем компонент RadioInput в yield и используем хэлпер component для передачи ему свойств по умолчанию, которые вызывающий может переопределить:

В нашем примере мы используем yield функциональность Ember.js, а также параметры блока (Block parameters) (см. подробнее раздел Block Content на сайте документации). Мы можем передать произвольные значения компонентному блоку, а затем потребители сами могут решать, какие из предоставленных значений они хотят использовать. Возможно думать о полученных значениях как о функциях компонента, которые могут быть вызваны потребителями по необходимости.

В нашем примере мы получаем компонент RadioInput, которому мы передаем параметры по умолчанию, а также currentValue компонента RadioInputForm. Чтобы создать пользовательский интерфейс, мы перебираем данные, которые определяют доступные элементы, и отображаем RadioInput для каждого из них.

Резюме по паттерну Compound Component

Как мы видим, данный паттерн можно использовать как с Ember.js, так и с React. Возможно, его немного проще использовать с Ember.js, чем с React, потому что вам не нужно знать тонкости React.Children и React.cloneElement. Но при этом нужно знать про yield и блочные параметры.

В отличие от непосредственного рендеринга дочерних элементов, компоненты Ember.js могут передавать предопределенные значения, которые затем могут использоваться их потребителями в блоке этого компонентов (разметке обрамленной данным компонентом). Такой гибкий способ передавать свойства блоку компонентов в Ember избавляет от проблемы, которая есть у реализации шаблона Compound Component на React.

В реализации React вы не можете добавить контент, отличный от RadioInput. Вы не можете изменить разметку компонента, не изменив его реализацию, потому что разметка напрямую связана с React.Children. Реализация ожидает, что каждый дочерний элемент RadioInputForm будет объектом RadioInput потому что именно RadioInputForm отвечает за отображение своих дочерних элементов и изменяет то, что отображается при вызове его метода render().

Ember.js, напротив, более гибок. Родительский компонент подготавливает вызываемые (callable) значения в свой блок, а потребители затем могут их использовать по своему усмотрению. По этой же причине мы можем получить прямой доступ к currentValue компонента RadioInputForm. Это делает композицию компонентов проще, чем с React и приводит нас к следующему паттерну.

Паттерн Flexible Compound Components

Реализация паттерна в React

Усложним наш пример, добавив на страницу кнопку Submit.

Совместное использование состояния между родительскими и дочерними компонентами в React нетривиально. Чтобы сделать это гибко - т.е. независимо от того, где дочерние элементы отображаются в дереве компонентов - вы можете использовать Context API.

Здесь мы создаем Context - место для хранения состояния, к которому дочерние компоненты будут получать доступ через пару Provider и Consumer:

Здесь важно проявлять аккуратность при передаче состояния в Provider. Поскольку React выполняет рендеринг каждый раз, вы должны помнить, что всегда нужно передавать, this.state. Не передавать состояние как новый объект, к примеру <RadioImageFormContext.Provider value={{ currentValue: this.state.currentValue, onChange: this.onChange }}> По этой же причине, при использовании функциональных компонентов вы должны делать что-то похожее на useMemo. За деталями можно обратиться к этому варианту кода на CodeSendBox

Результат реализации паттерна в React через классы (прямая ссылка на codesandbox):

Реализация паттерна в Ember.js

Ember.js с его yield функциональностью не имеет тех проблем, что и реализация React. Расширение нашего примера с помощью кнопки Submit тривиально (прямая ссылка на codesandbox):

Мы добавляем в yield новый компонент Submit, которому мы передаем, currentValueкак мы делали это раньше с RadioInput. Разработчикам Ember.js не нужно изучать новые концепции, для того чтобы не зависеть от порядка рендеринга дочерних элементов.

Выводы по паттерну Flexible Compound Component

Как мы видим, React позволяет разработчикам создавать абстракции компонентов независимо от порядка дочерних элементов в дереве компонентов. Но Ember.js упрощает реализацию этого паттерна и позволяет разработчикам попасть в воронку успеха.

Мне, как внешнему наблюдателю экосистемы, React в этом примере кажется очень низкоуровневым. Есть несколько способов сделать что-то похожее - использовать React.Children или Context-API - и для новичков вроде меня нетривиально понять, в чем преимущества определенных подходов или что один из них намного лучше другого, почему вообще существует другой. Вам также важно понимать, как React обрабатывает ререндеры - в оригинальном посте есть несколько упоминаний этого момента.

Ember.js, напротив, позволяет вам передавать значения через yield, о которых вы можете думать как о готовых к вызову функциях внутри компонентного блока - это очень похоже на каррированием в javascript, становится понятно, откуда берутся определенные значения. К примеру, в контексте паттерна, rif.Submit прозрачно привязан к RadioInputForm as |rif| разделу шаблона.

Теперь переходим к последнему паттерну в нашем обсуждении.

Паттерн Provider

Реализация паттерна в React

Паттерн Provider - это решение для обмена данными по всему дереву компонентов React. В нем используются обе предыдущие концепции - React Context API и render props.

React и Ember.js используют однонаправленный поток данных, поэтому при объединении нескольких компонентов вам иногда приходится последовательно передавать общее состояние из родительских компонентов на дочерние. Такое явление называется prop drill

Чтобы обойти проблему в React, который в отличие от Ember.js с его абстракцией Services , не имеет встроенной версии глобального состояния, которое вы можете присоединить (inject) к компонентам. Вы должны использовать React Context API или сторонние библиотеки управления состоянием (например Redux, MobX, Recoil и тд.), чтобы сделать состояние доступным для компонентов на разных уровнях дерева.

В публикации представлен простой пример странички профилей собак с загрузкой данный. Код примера доступен в этом Codesendbox. Компонент DogDataProvider иллюстрирует паттерн. Этот компонент будет отвечать за загрузку данных - в данном случае объектов, которые содержат данные для разных собак, и предоставлять их остальным компонентам.

В этом примере мы создали Contextи функциональный компонент, который хранит состояние асинхронного запроса загрузки данных. Затем мы передаем состояние функционального компонента объекту DogDataProviderContext.Provider,чтобы сделать его доступным для потребителей провайдера.

Для этого создадим свой хук:

Поскольку нам нужно убедиться, что DogDataProviderContext существует в области видимости всех компонентов, которые используют этот хук, мы добавляем проверку на undefined. Теперь мы не забудем обернуть потребителей состояния в DogDataProvider.

Собираем все вместе:

В этом примере мы видим, что всё, что будет использовать состояние DogProvider, нужно обернуть в DogProviderState. Внутри компонента Profile мы получаем доступ к этому состоянию и отображаем состояние загрузки, состояние ошибки или загруженные данные. В зависимости от состояния асинхронного запроса.

Реализация паттерна в Ember.js

Props drill также является проблемой в кодовых базах Ember.js. С помощью Ember.js вы также можете создавать абстракции компонентов, которые охватывают несколько уровней и которым требуется доступ к состоянию определенного родителя (см. пример из официального туториала). Поэтому использование данного паттерна в Ember.js так же полезно, как и в React. Однако в Ember.js вам не нужно создавать специальный компонент, чтобы иметь возможность использовать глобальное состояние приложения, потому что, в отличие от React, у фреймворка есть и другие абстракции, не только Component.

Для хранения глобального состояния приложения в Ember.js обычно используется Service . Это синглтон, который вы можете внедрять в разные части вашего приложения с помощью внедрения зависимостей (dependency injection) . Это "ленивые" объекты, которые будут инициализированы только при первом обращении к ним.

В нашем примере мы создадим сервис DogData и напрямую присоединим его в наш компонент Profile:

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

Внедрение зависимостей (DI) в Ember.js также упрощает тестирование компонентов, использующих DogDataService. В тестах мы просто инжектим Mock вместо реального DogDataService c легкостью тестируем наши компоненты.

Резюме по паттерну Provider

Данный паттерн - полезный инструмент для использования в архитектуре одностраничного приложения. Его можно использовать как с React, так и с Ember.js. Однако в Ember.js нам не нужно создавать столько служебного кода, сколько нужно при использовании React. Фреймворк предоставляет абстракцию для хранения глобального состояния приложения, которую компоненты-потребители используют с помощью внедрения зависимостей. Это упрощает доступ к общему состоянию и дает возможность тестировать компоненты, которые получают доступ к этому общему состоянию изолированно.

Заключение

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

Все перечисленные в этом посте паттерны компоненты React могут быть реализованы в Ember.js. Хотя я немного предвзято отношусь к Ember.js, но считаю, что эти паттерны проще использовать с ним. Вам придется писать гораздо меньше служебного кода по сравнению с React.

Для меня было удивительно, сколько работы вам нужно проделать в React для решения типичных моментов, возникающих при создании простых в использовании абстракций компонентов. В отличие от того, как в обсуждениях обычно воспринимается React по сравнению с Ember.js, я думаю, что многим будет интересно взглянуть на решения в этом посте, чтобы понять, насколько удобен Ember.js в разработке, особенно в текущей редакции Octane. Сказывается то, что Ember.js построен вокруг определенных конвенций, которые помогают вам попасть в упомянутую воронку успеха, вместо того, чтобы давать вам только низкоуровневые примитивы для решения общих задач множеством способов с неочевидными компромиссами.

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

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

Если этот пост вызвал у вас любопытство и вы считаете, что Ember.js подходит для вашего следующего проекта, не стесняйтесь связаться с нами (от переводчика: есть телеграмм канал на русском языке). Мы здесь, чтобы помочь командам создавать амбициозные приложения и помочь вам прототипировать решения, чтобы вы получили представление о том, какая библиотека или фреймворк лучше всего подходят для вашей проблемной области.

Комментарии (0)