После того как React решил заменить классовые компоненты на функциональные с блэджеком и хуками, он, неизбежно, привел сообщество к холивару, поскольку у каждого вида компонентов есть свои достоинства и недостатки. Сегодня я решил написать решение, которое может прекратить этот холивар, и, разумеется, начать новый.
Давайте посмотрим в чем преимущества и недостатки каждого из вида компонентов:
Классовые:
+ Есть наследование
+ Удобно расположить разные части кода по разным методам
- State может быть только один
- Везде надо писать this
- В целом больше кода
Функциональные:
+ Короче
+ Можно делать много state-ов
+ useEffect с зависимостями выглядит логичнее всяких componentDidUpdate
- Весь код свален в одну функцию
- Вместо наследования не очень наглядные решения вроде пользовательских хуков
А есть все совместить? Я подумал и написал react‑fcomponents. Сразу скажу, что проект на ранней стадии и я даже не уверен, что буду им пользоваться сам, хотя возможно и буду. Но в этой статье я в любом случае объясню как им пользоваться.
Что такое react‑fcomponents? Если кратко, это класс, внутри которого работают хуки и есть несколько дополнительных удобств. Сам класс на данный момент очень короткий.
Давайте посмотрим как им пользоваться на следующем примере:
//ExampleClass.js
import { useEffect, useState } from "react";
import FComponent, { make } from "react-fcomponents";
class ExampleClass extends FComponent {
useTest = (...args) => {
useEffect(() => {
console.log('mounted');
}, []);
return useState(...args);
}
useHooks() {
this.useTest();
this.useState('test', 0);
}
render() {
return <div>{this.props.text}<button onClick={
() => {
this.setStates.test(this.states.test + 1)
}
}>{this.states.test}</button></div>;
}
}
export default make(ExampleClass);
//App.js
import ExampleClass from './ExampleClass';
function App() {
return (
<div>
<ExampleClass text="Hello World" />
<ExampleClass text="Hello World2" />
</div>
);
}
export default App;
Начнем с конца. Как можно заметить, экспортируется не ExampleClass, а make(ExampleClass). Это делается для того, чтобы React не решил что это классовый компонент. Внутри make создает новый экземпляр класса, и возвращает хитрую функцию, которая в конечном счете запускает метод render. Но React считает компонент функциональным!
Теперь посмотрим на метод render:
render() {
return <div>{this.props.text}<button onClick={
() => {
this.setStates.test(this.states.test + 1)
}
}>{this.states.test}</button></div>;
}
С чего тут можно начать. Во‑первых, this.props работает. Кстати, это не обязательно — можно написать render (props) и далее работать без this. Но вот если вы будете работать с this в других методах класса, это пригодится.
Далее, в данном примере я не добавил в render хуки (они в методе useHooks, про который я расскажу позже), но их там можно спокойно использовать и они там будут работать. Вообще, render это содержимое функционального компонента и делать в нем можно все то же, что в функциональном компоненте.
Далее, мы видим весьма необычный метод работы со стейтами. Есть некие this.states.test и this.setStates.test. Очевидно, что это чтение и запись стейта, но откуда оно взялось? Для этого рассмотрим второй метод класса — useHooks:
useHooks() {
this.useTest();
this.useState('test', 0);
}
useHooks — это опциональный метод класса, код в котором вызывается перед вызовом кода render. Предполагается, что в него вы положите вызовы хуков, чтобы не захламлять ими render. Здесь я вызываю два хука — this.useTest и this.useState. Первый рассматривать не вижу смысла — там просто какие‑то обычные useState и useEffect. А вот второй интереснее.
this.useState — это встроенный хук, который принимает на вход уникальное имя стейта и его значение по‑умолчанию. Как можно догадаться, внутри this.useState происходит вызов обычного React.useState, а далее его значение и сеттер кладутся в this.states[имя стейта] и this.setStates[значение стейта] соответственно. В итоге в любом методе класса появляется возможность чтения и записи стейта по его имени.
Что важно, этим тоже пользоваться не обязательно. Вы вполне можете вызывать обычный React.useState и класть его результаты в this. Просто использование this.useState может быть удобнее.
В общем, это пока все что я сделал. Результат, на мой взгляд, взял лучшее из обеих миров. Часто ли это придется использовать, пока не знаю. Но мне кажется, это мне пригодится.
Комментарии (11)
Daseron
21.04.2023 17:14+1Зачем возвращаться к устаревшему Реакту? Функциональные компоненты намного удобнее старых классовых.
iOneM
21.04.2023 17:14+1Наследование в реакте - вершина антипаттернов, да и в целом это антипаттерн всего ООП при проектировании подобных сущностей.
Базовый принцип к которому аппелирует реакт - favor composition over inheritance. В остальном желание «Удобно расположить разные части кода по разным методам» - чаще всего тоже выстреливает в ногу. Когда в методах жизненного цикла появляется несколько цепочек не связанной напрямую функциональностей.
Забавный факт, но при переходе от классовых компонентов к функциональным, все проходят одну и ту же боль - нужно перестраивать мышление, с хуками возможно писать очень чистые и понятные компоненты, но для этого необходимо научиться выделять кастомные хуки. При этом возврат к классовым после полноценного перехода к хукам становится дорогой к громоздкому и грязному коду.
Zimtir
21.04.2023 17:14Все куда проще:
Когда «все» писали на C++, Java, .NET и Erlang из-за «нишевости», то «новые» языки чаще всего сваливались в мультипарадигму, как итог использовались для прототипирования
Никто не хотел доверять банковскую систему на Node.js и Django
Сейчас, в эпоху стартапов, где бизнес тестирует гипотезы, а банки — это очередной блокчейн, позволительно думать, что надо все писать на этом стеке, сравните качество систем.
А вот про стили написания на жабаскрипте:
— ООП кривое (наследование прототипное, где абстрактные классы? А зачем они, в погоне за хайпом .NET и жаба вводят реализацию в интерфейсы; полиморфизм, а где перегрузка методов? Инкапсуляция? Ой, а у нас hosting)
— ФП кривое (а где каррирование нормальное? А где пайп оператор? Библиотеку поставить? А не будет как с cross env? А где паттерн матчинг? А где атомы? А атомы есть, это символы, только пользоваться ими не умеют, лейблы используют чтобы из н^2 выйти
И я не говорю про отсутствие мультитрендинга и так далее
В общем, жабаскрипт — это все еще прототипирование, спорить о парадигмах в нем глупо, так как его задача — гипотезы и стартапы, хотя у Амазона с лямбдами есть свои плюсы.
Писать нужно в код-стайле команды и хороших практик. Половина паттернов не пригодны, SOLID не реализуется.
А, да, большая доля рынка айти сейчас это стартапы и малый бизнес, оттуда и синдромы самозванцев у ребят, порог входа низкий.
И как говорят классики — выстраивать карьеру возле одной библиотеки для языка прототипов — это вилы
Привет, Event Loop
19Zb84
Мне кажется это достаточно спорно звучит. Хуки это самое удобное что есть в реакте.
nin-jin
Я аж чаем поперхнулся. Жёсткие ограничения на места вызова хуков. Ручное указание всех зависимостей. Ререндер всего компонента, на изменение любого стейта. Намертво прибиты к реакту, без возможности переиспользования в других местах. Пляски с бубном для доступа к их состоянию вне рендеринга компонента.
iOneM
Жесткое ограничение на вызовы: минус, согласен тут спорно, абстракция ради достижения цели, чаще всего интуитивная.
Ручное указание зависимостей: плюс, а вы пользовались когда-нибудь gdsfp в классовых компонентах с несколькими стейтами? Ручное указание зависимостей - гигантский скачок вперед для реакта после этого кошмара.
Рендер всего компонента на изменение стейта - выдуманный недостаток. А тут-то что не так? Классовые тоже перерендериваются при изменении стейта.
Намертво прибиты к реакту - тоже выдуманный пункт, классовые методы тоже прибиты к реакту, точнее даже к конкретному компоненту. Единственным способом переиспользования классовых методов жизненного цикла - является как раз написание HOC’ов. А вот хуки можно переиспользовать в других компонентах и даже хуках. В целом если встает проблема «прибитых к реакту хуков», то вероятно нет разграничения функциональности между утиллитами и хуками. Хуки должны реализовывать только части жизненного цикла компонентов.
Пляски с бубном для доступа к их состоянию вне рендеринга компонента - очень надеюсь, что в данном пункте не подразумевается доступ к методам классового компонента посредством ref’ов, тоже плохой паттерн, повышающий связность и размазывающий логику.
Если уж надо назвать реальные минусы хуков - то это в первую очередь «казуальность». Хуки дают так много свободы, что комьюнити начала использовать хуки для решения проблемы здесь и сейчас, теряется грань между интерфейсами, размывается разграничение ответственностей - это все влечет за собой деградацию кодовой базы в руках слабых IT-специалистов.
Если чуть обобщить, то реакт никогда не славился следованием принципу «pit of success”, при котором самое логичное действие непременно приведет к успеху, а хуки в этом плане еще один шаг в противоположном направлении.
nin-jin
Откройте уже для себя любую из десятков библиотек с автоматическим отслеживанием зависимостей.
Только того, что находится в state, а не вообще всех полей объекта.
Объекты, лежащие в полях классового компонента, к реакту не прибиты.
iOneM
Давайте не будем подменять понятия и от "ручного указания зависимостей" как недостаток функциональных компонентов перепрыгивать сразу к библиотекам стейт менеджеров. Стейт менеджеры уходят от изначальной проблемы и используются в любых компонентах, а статья и комментарии фокусируются на преимуществах и недостатках функциональных и классовых компонентов. Пример с gdsfp был как раз в качестве возможного способа управления зависимостями при получении новых состояний.
Возможно у вас другое определение слову "перерендеривается". В реакте перерендеринг - это процесс построения виртуального дерева на основе результата рендера компонента. Ни в классовых, ни в функциональных, нет ничего похожего на перерендеринг только того, что находится в state. Если под словом рендеринг вы подразумеваете "исполнение" тела функционального компонента, на другую чащу весов выставляя то, что классовый компонент не пересоздается, то я, вероятно, вас огорчу - это разные подходы к жизненному циклу, причем в случае функциональных компонентов тело все равно выполняется быстрее, а при корректно указанных зависимостях дополнительных побочных эффектов быть не должно. Что, кстати, в случае работы с несколькими состояниями в классовых компонентах - через тот же gdsfp становится настоящим адом, ведь он будет вызываться даже при изменении стейта.
Тут стоит указать пример использования и желательно не отходить от темы различий между классовыми и функциональными компонентами. Объекты можно хранить и в функциональных компонентах, можно даже использовать вложенные сервисы, заворачивая их подписки и отписки в классовые методы, но от этого мы не делаем сами методы класса переиспользуемыми, мы добавляем еще одну абстракцию, чтобы не прибивать гвоздями часть функционала. В классовых компонентах без наследования, миксинов или ХОК-ов нельзя вынести набор методов жизненого цикла для переиспользования. Конечно, всегда можно вынести весь "умный" компонент и переиспользовать его, но это не отличается от функциональных компонентов.
С другой стороны хуки позволяют выносить блоки жизненного цикла в отдельные сущности, без использования десятка промежуточных инструментов, это дополнительная гибкость, она несет в себе как пользу, так и вред - вот это уже более интересная тема для обсуждения.
strannik_k
Я писал статью про другие способы, которые при желании можно применить и к классовым компонентам реакта: Техники повторного использования кода и разбиения сложных объектов на составные
HOC - это аналог паттерна "декоратор" для функциональных компонентов.
Хук - это аналог паттерна "стратегия" для функциональных компонентов. Только у функциональных компонентов недостаток в том, что в них нельзя заменить хук (стратегию), а в объектах/классах при использовании паттерна "стратегия", можно.
Классовые методы можно выносить в отдельные объект и переиспользовать в других компонентах. Просто из коробки нет общего подхода для такого переиспользования.
Касательно хуков. Группа хуков, вызов которых прописан в конкретном функциональном компоненте - это фиксированный список "стратегий". В геймдеве давно есть похожий подход, но применяемый к объектам/классам. Но там используется не фиксированный список, а динамический. Плюс более грамотное разделение ответственности. В реакте же компонент является и контейнером для стратегий (хуков), содержит в себе разметку, а также позволяет писать логику в самих компонентах. Такое объединение функционала является недостатком как функциональных, так и в классовых компонентах. Это нарушение принципа SRP, приводящее к уменьшение гибкости компонента и менее качественному коду пользователей react-а.
iOneM
Про то, что хуки это исключительно стратегия не соглашусь, при помощи хуков можно инкапсулировать практически любые сервисы и инструменты. Использование хуков в качестве стратегии - частный случай.
Поинт про невозможность замены хуков - достаточно интересен, ведь такая проблема в общем виде есть, однако если углубиться в механизмы при помощи которых подобная замена реализуема в классовых компонентах - это либо наследование, либо мутация. Все современные фреймворки ушли в декларативный подход, он доказал себя максимально эффективным в плане управления сложностью и в этом подходе нет места ни мутациям, ни наследованию.
Не буду спорить, что представители секты последователей наследования и мутаций все еще среди нас - спорить с ними по этому поводу я не буду)