Pull to refresh

Почему в Ember не нужны React-хуки

Reading time9 min
Views2.4K
Original author: Michael Klein

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

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

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

Дисклеймер

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

Изучаемая проблема - композиция абстракций

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

Обычно, когда речь заходит о переиспользовании на стороне клиента, люди думают о дизайн-системах, но UI-компоненты для репрезентации далеко не единственная вещь, которую вам захочется переиспользовать. В вашем приложении будет появляться похожее поведение, которое будет напрашиваться на повторное использование. Например, запуск асинхронного взаимодействия или показ уведомления исходя из определенного действия пользователя. Вы, наверняка, заходите переиспользовать подобные вещи.

Поэтому когда речь идет о хороших абстракциях в одностраничных приложениях, это означает не только создание библиотеки UI-компонентов, таких как кнопки, табы и прочее. Благоразумнее задуматься и о переиспользовании логики работы с состоянием (stateful logic).

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

До создания хуков, по словам core-команды React, библиотека не предоставляла хороших абстракций для сложной логики состояний:

"React не предоставляет способа "присоединить" переиспользуемое поведение к компоненту (например, соединить его с хранилищем (store). Если вы работали с React какое-то время, вы, наверняка знакомы с такими паттернами, как render props или компоненты высокого порядка (high-order components), которые пытаются решить эту проблему. Но эти паттерны требуют от вас перестраивать подключаемые компоненты, что может быть запутанным и усложняющим чтение кода.

Поэтому давайте попытаемся ответить, являются ли хуки единственным хорошим способом распространить абстракции по вашему коду? И как это реализуется в Ember.js?

Пример - Абстракция асинхронности

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

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

Чтобы привести имплементации в React и Ember к сравнимому состоянию, я решил смоделировать асинхронное поведение нашего приложения с помощью statechart.

Пойдя таким путем мы будем уверены, что компоненты будут вести себя одинаково и мы сможем лучше сравнить реализации. Будем использовать популярную библиотеку XState для моделирования графа-состояния (statechart). Вот граф-состояние, который мы будем использовать в этом посте (прямая ссылка):

Данный граф-состояние делает не особо много интересного. У нас четыре различных состояния нашего Async-компонента - idle, busy, success и error. Когда мы в idle, мы можем запустить функцию async-переключателя, которую мы затем можем передать в XState Machine через context этой машины. Этот запрос может либо быть resolve, либо reject, что приведет нас либо в состояние success, либо - в error . Когда запрос удачен мы вызовем обработчик onSuccess. Когда же он не удается, запускается обработчик onError.

React

Во-первых, мы сравним две реализации компонентов, моделирующих асинхронное поведение в React. Одна из них будет использовать React.Component, а другая - React Hooks. Мы сделаем это, потому что в официальной документации React хуки представлены как более удачный способ реализации логики состояния. Я хочу протестировать эту гипотезу.

Обе версии показывают состояние, в котором находится Async взаимодействие и предоставляют кнопку, которой можно его запустить. После нажатия на кнопку состояние случайным образом будет переключаться на resolve или reject. Вы можететь попробовать и изучить обе реализации в этом Codesandbox (прямая ссылка):

Как видим, обе версии Async-компонентов ведут себя одинаково. Что побуждает нас сделать первый вывод нашего поста:

Осуществлять абстракцию логики через компоненты чистым образом (clean way) можно как с помощью хуков, так и с помощью React.Component

Углубимся в детали:

React.Component

Чтобы предоставить состояние асинхронного взаимодействия на уровень разметки будем использовать паттерн Провайдер:

Для начала нужно создать Async-компонент, который после рендеринга запустит XState-интерпретатор. Используя Context API мы создадим пару Provider/Consumer, предоставляющую свойство state компонента другим компонентам, желающим использовать Async-поведение.

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

Теперь перейдем к React-хукам.

React-хуки

Для того, чтобы создать свой хук useAsync создадим абстракцию на базе хука useMachine, который входит в состав библиотеки xstate/react. Создание собственных хуков поощряется документацией и является одним из продающих моментов всей концепции.

Как видите, довольно просто создать свой собственный хук. Мы используем хук useMachine, настраиваем свой интерпретатор XState и затем возвращаем объект, который можно будет использовать в коде нашего приложения, чтобы обрабатывать async-поведение.

Для этого мы создадим компонент, использующий хук useAsync. Рано или поздно что-то должно вызвать наш хук и не похоже, что это можно сдать из разметки напрямую, как это возможно с помощью компонента Async-провайдера.

Для меня реализация с использованием React-хуков не выглядит чище реализации на React.Component. Не увидел ничего того, что можно было бы сделать с помощью хуков и чего нельзя было бы сделать, используя классы. Также мне не показалось, что концепция хуков это концепция, более понятная, чем нативные классы. И некоторые люди из экосистемы React склонны с этим согласиться.

Ember.js

@glimmer/component - компонент, который хранит состояние

Ember не предоставляет абстракции на хуках, поэтому, чтобы запрограммировать async-поведение мы будем использовать подход, схожий с подходом для React.Component и создадим Async-компонент, который будет хранить состояние асинхронного взаимодействия.

Codesandbox c решением на Ember (прямая ссылка)

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

В нашем примере useMachine создает объект с состоянием XState интерпретатора. Мы присваиваем этот объект свойству экземляра компонента, чтобы пользоваться преимуществами автотрекинга Ember и добавляем геттеры, такие как isIdle, которые автоматически обновляются при изменении графа-состояния.

Чтобы сделать состояние Async-поведения доступным для потребителей, мы используем функциональность yield и предоставляем блоковые параметры внутри блока компонента. Потребляющие компоненты смогут сами решить, какие из свойств компонента Async они хотят использовать.

Дополнительный материал: Хуки в Ember? - @use и Resource

В экосистеме Ember есть функциональность напоминающая React-хуки. Обсуждение RFC было резюмировано в статье "Введение @use" . Используя декоратор @use и классы на основе Resource можно также реализовать абстракции для состояния, которые потом могут быть использованы в других объектах приложения.

Важной особенностью @use и в контексте приложения Ember.js является то, что Resource будет привязан к жизненному циклу объекта, вызвавшего @use. Это может быть полезно и удобно, когда вы пишите большие приложения. Но в сообществе еще не закончено обсуждение, является ли эта абстракция более удачной, чем уже существующие в экосистеме.

Поэтому, если коротко, @use и Resource не являются Ember ответом на React-хуки. Честно говоря, я лично не увидел ничего революционного в этой парадигме. Выглядит, как будто это своего рода заплатка на те недостатки, которые были в React изначально. Речь идет о необходимости написания служебного кода для передачи состояния и логики между различными частями приложения. В варианте с React.Component вы вынуждены положиться на Context API. Неудивительно, что намучившись с написанием бойлерплейта для Provider/Consumer многие разработчики поспешили использовать хуки. Но концептуально эта парадигма не выглядит чем-то более привлекательным, чем абстракции, которые уже есть в Ember.js

Резюме

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

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

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

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

  1. yield-функционал Ember позволяет передать произвольные нижестоящим компонентам данные, которые те могут использовать по своему усмотрению. Это позволяет легко строить компоненты-провайдеры.

  2. Когда необходимо получить доступ к глобальному состоянию, Ember.js предоставляет сервисы, синглтоны, которые можно подключать к различным частям приложения с помощью dependency injection.

  3. Создание абстракций, использование этих абстракций из других абстракций и составление композиций не является революционной идеей. В Javascript и до этого можно было создавать объекты с состоянием, следующих принципу единственной ответственности, и составлять из них композиции. И для этого вовсе не требуется знать правила написания хуков.

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

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

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

Tags:
Hubs:
Rating0
Comments2

Articles