Ненормальный React: давайте совместим классовые и функциональные компоненты в одно

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

Давайте посмотрим в чем преимущества и недостатки каждого из вида компонентов:

Классовые:

Функциональные:

А есть все совместить? Я подумал и написал 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 может быть удобнее.

В общем, это пока все что я сделал. Результат, на мой взгляд, взял лучшее из обеих миров. Часто ли это придется использовать, пока не знаю. Но мне кажется, это мне пригодится.

@freehabr
21.04.2023 17:49 UTC
Первоисточник

Комментарии

@
21.04.2023 17:14 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@nin-jin
21.04.2023 21:31 UTC
+1

Я аж чаем поперхнулся. Жёсткие ограничения на места вызова хуков. Ручное указание всех зависимостей. Ререндер всего компонента, на изменение любого стейта. Намертво прибиты к реакту, без возможности переиспользования в других местах. Пляски с бубном для доступа к их состоянию вне рендеринга компонента.

22.04.2023 01:11 UTC
0

Жесткое ограничение на вызовы: минус, согласен тут спорно, абстракция ради достижения цели, чаще всего интуитивная.

Ручное указание зависимостей: плюс, а вы пользовались когда-нибудь gdsfp в классовых компонентах с несколькими стейтами? Ручное указание зависимостей - гигантский скачок вперед для реакта после этого кошмара.

Рендер всего компонента на изменение стейта - выдуманный недостаток. А тут-то что не так? Классовые тоже перерендериваются при изменении стейта.

Намертво прибиты к реакту - тоже выдуманный пункт, классовые методы тоже прибиты к реакту, точнее даже к конкретному компоненту. Единственным способом переиспользования классовых методов жизненного цикла - является как раз написание HOC’ов. А вот хуки можно переиспользовать в других компонентах и даже хуках. В целом если встает проблема «прибитых к реакту хуков», то вероятно нет разграничения функциональности между утиллитами и хуками. Хуки должны реализовывать только части жизненного цикла компонентов.

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

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

Если чуть обобщить, то реакт никогда не славился следованием принципу «pit of success”, при котором самое логичное действие непременно приведет к успеху, а хуки в этом плане еще один шаг в противоположном направлении.

22.04.2023 06:38 UTC
0

а вы пользовались когда-нибудь gdsfp в классовых компонентах с несколькими стейтами? Ручное указание зависимостей - гигантский скачок вперед для реакта после этого кошмара.

Откройте уже для себя любую из десятков библиотек с автоматическим отслеживанием зависимостей.

Классовые тоже перерендериваются при изменении стейта.

Только того, что находится в state, а не вообще всех полей объекта.

тоже выдуманный пункт, классовые методы тоже прибиты к реакту, точнее даже к конкретному компоненту

Объекты, лежащие в полях классового компонента, к реакту не прибиты.

23.04.2023 12:12 UTC
0

Откройте уже для себя любую из десятков библиотек с автоматическим отслеживанием зависимостей.

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

Только того, что находится в state, а не вообще всех полей объекта.

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

Объекты, лежащие в полях классового компонента, к реакту не прибиты.

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

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

23.04.2023 09:45 UTC
+1

Единственным способом переиспользования классовых методов жизненного цикла - является как раз написание HOC’ов.

Я писал статью про другие способы, которые при желании можно применить и к классовым компонентам реакта: Техники повторного использования кода и разбиения сложных объектов на составные

HOC - это аналог паттерна "декоратор" для функциональных компонентов.

Хук - это аналог паттерна "стратегия" для функциональных компонентов. Только у функциональных компонентов недостаток в том, что в них нельзя заменить хук (стратегию), а в объектах/классах при использовании паттерна "стратегия", можно.

Намертво прибиты к реакту - тоже выдуманный пункт, классовые методы тоже прибиты к реакту, точнее даже к конкретному компоненту.

Классовые методы можно выносить в отдельные объект и переиспользовать в других компонентах. Просто из коробки нет общего подхода для такого переиспользования.

Касательно хуков. Группа хуков, вызов которых прописан в конкретном функциональном компоненте - это фиксированный список "стратегий". В геймдеве давно есть похожий подход, но применяемый к объектам/классам. Но там используется не фиксированный список, а динамический. Плюс более грамотное разделение ответственности. В реакте же компонент является и контейнером для стратегий (хуков), содержит в себе разметку, а также позволяет писать логику в самих компонентах. Такое объединение функционала является недостатком как функциональных, так и в классовых компонентах. Это нарушение принципа SRP, приводящее к уменьшение гибкости компонента и менее качественному коду пользователей react-а.

23.04.2023 12:25 UTC
0

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

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

Не буду спорить, что представители секты последователей наследования и мутаций все еще среди нас - спорить с ними по этому поводу я не буду)

24.04.2023 21:07 UTC
+1

однако если углубиться в механизмы при помощи которых подобная замена реализуема в классовых компонентах - это либо наследование, либо мутация.

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

Про то, что хуки это исключительно стратегия не соглашусь

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

@Daseron
21.04.2023 19:20 UTC
+1

Зачем возвращаться к устаревшему Реакту? Функциональные компоненты намного удобнее старых классовых.

@iOneM
22.04.2023 00:49 UTC
+1

Наследование в реакте - вершина антипаттернов, да и в целом это антипаттерн всего ООП при проектировании подобных сущностей.

Базовый принцип к которому аппелирует реакт - favor composition over inheritance. В остальном желание «Удобно расположить разные части кода по разным методам» - чаще всего тоже выстреливает в ногу. Когда в методах жизненного цикла появляется несколько цепочек не связанной напрямую функциональностей.

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

@mogilevtsevdmitry
22.04.2023 14:56 UTC
0

А где это реально могло бы пригодиться?

@the_homeless_god
23.04.2023 21:53 UTC
+1

Все куда проще:

Когда «все» писали на C++, Java, .NET и Erlang из-за «нишевости», то «новые» языки чаще всего сваливались в мультипарадигму, как итог использовались для прототипирования

Никто не хотел доверять банковскую систему на Node.js и Django

Сейчас, в эпоху стартапов, где бизнес тестирует гипотезы, а банки — это очередной блокчейн, позволительно думать, что надо все писать на этом стеке, сравните качество систем.

А вот про стили написания на жабаскрипте:

— ООП кривое (наследование прототипное, где абстрактные классы? А зачем они, в погоне за хайпом .NET и жаба вводят реализацию в интерфейсы; полиморфизм, а где перегрузка методов? Инкапсуляция? Ой, а у нас hosting)

— ФП кривое (а где каррирование нормальное? А где пайп оператор? Библиотеку поставить? А не будет как с cross env? А где паттерн матчинг? А где атомы? А атомы есть, это символы, только пользоваться ими не умеют, лейблы используют чтобы из н^2 выйти

И я не говорю про отсутствие мультитрендинга и так далее

В общем, жабаскрипт — это все еще прототипирование, спорить о парадигмах в нем глупо, так как его задача — гипотезы и стартапы, хотя у Амазона с лямбдами есть свои плюсы.

Писать нужно в код-стайле команды и хороших практик. Половина паттернов не пригодны, SOLID не реализуется.

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

И как говорят классики — выстраивать карьеру возле одной библиотеки для языка прототипов — это вилы

Привет, Event Loop