[Перевод] Разбираемся с перехватчиками в React
Привет, Хабр!
Мы с чувством невероятной гордости и облегчения сегодня вечером сдали в типографию новую книгу о React
По этому поводу предлагаем вам немного сокращенный перевод статьи Дэна Абрамова (Dan Abramov), рассказывающего об использовании перехватчиков в 16-й версии React. В книге, которую мы сами уже ждем с нетерпением, об этом рассказано в 5-й главе.
На прошлой неделе мы с Софи Олперт представили на конференции React Conf концепцию «перехватчиков», после чего последовал подробный разбор темы от Райана Флоренса.
Настоятельно рекомендую посмотреть эту пленарную лекцию, чтобы ознакомиться с кругом проблем, которые мы пытаемся решить при помощи перехватчиков. Однако, даже час вашего времени я ценю очень высоко, поэтому решил вкратце изложить в этой статье основные соображения по перехватчикам.
Примечание: перехватчики в React — вещь пока экспериментальная. Не нужно вникать в них прямо сейчас. Также учтите, что в этой публикации изложены мои личные взгляды, которые могут не совпадать с позицией разработчиков React.
Зачем нужны перехватчики?
Известно, что компонентная организация и нисходящий поток данных помогают организовать крупный UI в виде маленьких, независимых и многоразовых фрагментов. Однако, зачастую не удается раздробить сложные компоненты далее некого предела, поскольку логика сохраняет состояние и неизвлекаема в функцию или какой-то другой компонент. Иногда именно на это жалуются те, кто говорит, что в React не получается добиться «разделения обязанностей».
Такие случаи весьма распространены, и связаны, например, с анимацией, обработкой форм, подключением к внешним источникам данных и многими другими операциями, которые нам, возможно, потребуется совершать с нашими компонентами. Пытаясь решать такие задачи при помощи одних только компонентов, мы обычно получаем:
- Гигантские компоненты, которые сложно рефакторить и тестировать.
- Дублирование логики между различными компонентами и методами жизненного цикла.
- Сложные паттерны, в частности, рендеринг свойств (render props) и компоненты высшего порядка.
Мы считаем, что именно перехватчики наиболее перспективны для решения всех этих проблем. Перехватчики помогают организовать логику внутри компонента в виде многоразовых изолированных единиц:
Перехватчики соответствуют философии React (явный поток данных и композиция) и внутри компонента, а не только между компонентами. Вот почему мне кажется, что перехватчики естественным образом вписываются в модель компонентов React.
В отличие от таких паттернов как рендеринг свойств или компоненты высшего порядка, перехватчики не обременяют ваше дерево компонентов излишне глубокими вложениями. Также у них нет тех недостатков, которые присущи примесям.
Даже если на первый взгляд перехватчики вас коробят (точно как и меня поначалу!) рекомендую дать этому варианту шанс и поэкспериментировать с ним. Думаю, вам понравится.
Не разбухает ли React из-за перехватчиков?
Пока мы не рассмотрели перехватчики в подробностях, вы, возможно, волнуетесь, что добавление перехватчиков в React — это просто умножение сущностей. Это справедливая критика. Я думаю так: хотя, в краткосрочной перспективе действительно почувствуется лишняя когнитивная нагрузка (чтобы изучить их), в итоге вам станет только легче.
Если перехватчики приживутся в сообществе React, то на самом деле сократится количество сущностей, с которыми приходится управляться при написании приложений React. При помощи перехватчиков можно постоянно пользоваться функциями, а не переключаться между функциями, классами, компонентами высшего порядка и рендерингом компонентов.
Что касается увеличения размеров реализации, приложение React при поддержке перехватчиков увеличивается всего лишь примерно на ~1.5kB (min+gzip). Притом, что и само по себе это не слишком много, весьма вероятно, что при использовании перехватчиков размер вашей сборки даже уменьшится, поскольку код перехватчиков обычно минифицируется лучше, чем эквивалентный код с использованием классов. Нижеприведенный пример слегка экстремален, зато он доходчиво демонстрирует, почему все именно так (щелкните, чтобы развернуть весь тред):
В предложении по перехватчикам нет никаких революционных изменений. Имеющийся у вас код будет нормально работать, даже если вы начинаете использовать перехватчики в новоиспеченных компонентах. На самом деле, именно это мы и рекомендуем: ничего глобально не переписывайте! Разумно будет подождать, пока использование перехватчиков устоится во всем критичном коде. Все-таки, будем благодарны, если вы сможете поэкспериментировать с альфа-версией 16.7 и оставите нам отзывы на предложение по перехватчикам, а также сообщите о любых багах.
Что же это такое — перехватчики?
Чтобы понять, что же перехватчики, нужно вернуться на шаг назад и задуматься, что же такое переиспользование кода.
Сегодня в React-приложениях существует множество способов многоразового использования логики. Так, чтобы вычислить что-либо, можно писать простые функции, а затем вызывать их. Также можно писать компоненты (которые сами по себе могут быть функциями или классами). Компоненты более мощные, но при работе с ними требуется отображать некоторый UI. Поэтому при помощи компонентов неудобно передавать невизуальную логику. Так мы и приходим к сложным паттернам вроде рендеринга свойств и к компонентам высшего порядка. Не стал бы React проще, если бы в нем существовал всего один общий способ переиспользования кода, а не так много?
Кажется, что функции идеально подходят для многоразового использования кода. Передача логики между функциями наименее затратна. Однако, внутри функций не может храниться локальное состояние React. Нельзя извлечь поведение вроде «отслеживать размер окна и обновлять состояние» или «анимировать значение в течение некоторого времени» из компонента-класса без переструктуризации кода или без введения таких абстракций как наблюдаемые объекты (Observables). Оба подхода лишь усложняют код, а React мил нам своей простотой.
Перехватчики решают именно эту проблему. Благодаря перехватчикам, можно использовать возможности React (например, состояние) из функции — вызвав ее всего раз. В React предоставляется несколько встроенных перехватчиков, соответствующих «кирпичикам» React: состоянию, жизненному циклу и контексту.
Поскольку перехватчики — обычные JavaScript-функции, можно комбинировать встроенные перехватчики, предоставляемые в React, создавая «собственные перехватчики». Таким образом, сложные проблемы удастся решать единственной строчкой кода, а потом множить ее в вашем приложении, либо делиться ею в сообществе React
Внимание: строго говоря, ваши собственные перехватчики не входят в число возможностей React. Возможность писать собственные перехватчики естественным образом проистекает из самой их внутренней организации.
Покажите же код!
Допустим, мы хотим подписать компонент на текущую ширину окна (например, для вывода иного контента или более узкой области просмотра).
Подобный код сегодня можно писать несколькими способами. Например, сделать класс, создать несколько методов жизненного цикла или может быть даже прибегнуть к рендерингу свойств или применить компонент высшего порядка, если рассчитываете на многократное использование. Однако, думаю, ничто не сравнится с этим:
gist.github.com/gaearon/cb5add26336003ed8c0004c4ba820eae
Если вы читаете этот код, значит, он делает ровно то, что в нем написано. Мы используем ширину окна внутри нашего компонента, и React переотрисовывает ваш компонент, если он изменится. Именно за этим и нужны перехватчики — чтобы сделать компоненты подлинно декларативными, даже если в них содержится состояние и побочные эффекты.
Рассмотрим, как можно было бы реализовать этот собственный перехватчик. Мы могли бы воспользоваться локальным состоянием React, чтобы сохранить в нем актуальную ширину окна, и при помощи побочного эффекта задавать это состояние при изменении размера окна:
gist.github.com/gaearon/cb5add26336003ed8c0004c4ba820eae
Как показано выше, встроенные перехватчики React вроде useState
и useEffect
служат «кирпичиками». Мы можем использовать их непосредственно из наших компонентов, либо собирать из них собственные перехватчики, например, useWindowWidth
. Использование собственных перехватчиков представляется не менее идиоматичным, чем работа со встроенным API React.
Подробнее о встроенных перехватчиках рассказано в этом обзоре.
Перехватчики инкапсулируются — всякий раз при вызове перехватчика он получает изолированное локальное состояние внутри компонента, выполняемого в настоящий момент. В данном конкретном примере это не важно (ширина окна одинакова для всех компонентов!), но именно в этом и заключается сила перехватчиков! Они предназначены, чтобы разделять не состояние, а логику, сохраняющую состояние. Мы же не хотим ломать нисходящий поток данных!
Каждый перехватчик может содержать некоторое локальное состояние и побочные эффекты. Можно передавать данные между несколькими перехватчиками, точно как это обычно делается между функциями. Они могут принимать аргументы и возвращать значения, поскольку являются функциями JavaScript.
Вот пример анимационной библиотеки React, где мы экспериментируем с перехватчиками:
Обратите внимание, как в продемонстрированном исходном коде реализована ошеломительная анимация: мы передаем значения между несколькими собственными перехватчиками в рамках одной и той же функции рендеринга.
codesandbox.io/s/ppxnl191zx
(Этот пример подробнее разобран в данном руководстве.)
Благодаря возможности передавать данные между перехватчиками, они очень удобны для воплощения анимаций, подписок на данные, управления формами и работы с другими абстракциями, сохраняющими состояние. В отличие от рендеринга свойств или компонентов высшего порядка в случае с перехватчиками в вашем дереве рендеринга не создается «ложной иерархии». Они больше напоминают двумерный список «ячеек памяти», прикрепляемых к компоненту. Никаких дополнительных уровней.
А что же насчет классов?
На наш взгляд, собственные перехватчики — самая интересная деталь во всем предложении. Но, чтобы собственные перехватчики были работоспособны, React должен предусмотреть на уровне функций возможность объявлять состояние и побочные эффекты. Именно это и позволяют нам делать встроенные перехватчики вроде useState
и useEffect
. Подробнее об этом рассказано в документации.
Оказывается, что такие встроенные перехватчики удобны не только при создании собственных перехватчиков. Они также достаточны для определения компонентов в целом, поскольку предоставляют нам необходимые возможности — например, состояние. Вот почему мы хотели бы, чтобы в будущем перехватчики стали основным средством для определения компонентов React.
Нет, мы не планируем постепенно упразднять классы. У нас в Facebook используются десятки тысяч компонентов классов и нам (точно как и вам) совершенно не хочется их переписывать. Но, если сообщество React приступит к использованию перехватчиков, станет нецелесообразно сохранять два рекомендуемых способа написания компонентов. Перехватчики покрывают все те практические случаи, в которых используются классы, но дают большую гибкость при извлечении, тестировании и переиспользовании кода. Вот почему мы связываем с перехватчиками наши представления о будущем React.
А вдруг перехватчики — это магия?
Возможно, правила перехватчиков вас озадачат.
Хотя и не принято вызывать перехватчик на верхнем уровне, вы, вероятно, и сами не захотели бы определять состояние в условии, даже если бы могли. Например, состояние с привязкой к условию нельзя определить и в классе, и за четыре года общения с пользователями React я не слышал никаких жалоб по этому поводу.
Такой дизайн критически важен для внедрения собственных перехватчиков без попутного введения лишнего синтаксического шума или появления подводных камней. Мы понимаем, что с непривычки это сложно, но считаем, что данный компромисс приемлем, учитывая, какие возможности он открывает. Если вы не согласны — предлагаю поэкспериментировать самим и распробовать, как вам такой подход.
Мы используем перехватчики в продакшене уже месяц, чтобы проверить, не смутят ли программистов новые правила. Практика показывает, что человек осваивается с перехватчиками за считанные часы. Признаюсь, что и мне эти правила на первый взгляд казались ересью, но это ощущение быстро прошло. Именно такое впечатление у меня было и при первом знакомстве с React. (Вам не понравился React? И мне только на второй раз понравился.)
Обратите внимание: и на уровне реализации перехватчиков никакой магии тоже нет. Как пишет Джейми, она получается примерно такой:
gist.github.com/gaearon/62866046e396f4de9b4827eae861ff19
Мы ведем покомпонентный список перехватчиков, и переходим к следующему компоненту в списке всякий раз после использования перехватчика. Благодаря правилам перехватчиков, их порядок одинаков в любом механизме рендеринга, поэтому при каждом вызове мы можем обеспечить компонент правильным состоянием.
(В этой статье от Руди Ярдли все красиво объяснено в картинках!)
Возможно, вы задумывались –, а где React хранит состояние перехватчиков. Там же, где и состояние классов. В React есть внутренняя очередь обновлений, которая содержит истину в последней инстанции для каждого состояния, независимо от того, как именно вы определяете ваши компоненты.
Перехватчики не зависят от прокси и геттеров, которые так распространены в современных библиотеках JavaScript. Поэтому, можно утверждать, что в перехватчиках меньше магии, чем в других популярных подходах к решению таких задач. Не больше, чем в array.push
и array.pop
(в случае с которыми порядок вызовов также важен!)
Дизайн перехватчиков не привязан к React. На самом деле, уже через несколько дней после публикации предложения самые разные люди показали нам экспериментальные реализации такого же API перехватчиков для Vue, веб-компонентов и даже для обычных функций JavaScript.
Наконец, если вы фанатично преданы функциональному программированию, и вам неуютно на душе, когда React начинает полагаться на изменяемое состояние как на деталь реализации. Но, возможно, вас утешит, что обработку перехватчиков вполне можно реализовать и в чистом виде, ограничившись одними только алгебраическими эффектами (если бы в JavaScript они поддерживались). Естественно, на внутрисистемном уровне React всегда полагался на изменяемое состояние –, а ведь именно этого вы хотели бы избежать.
Независимо от того, какая точка зрения вам ближе — прагматичная или догматичная — надеюсь, что хотя бы один из этих вариантов кажется вам логичным. Наиболее важно, на мой взгляд, что перехватчики упрощают нам работу, а пользователям становится удобнее работать. Именно этим перехватчики меня так подкупают.