После того как 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)


  1. 19Zb84
    21.04.2023 17:14

    Вместо наследования не очень наглядные решения вроде пользовательских хуков


    Мне кажется это достаточно спорно звучит. Хуки это самое удобное что есть в реакте.


    1. nin-jin
      21.04.2023 17:14
      +1

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


      1. iOneM
        21.04.2023 17:14

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

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

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

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

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

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

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


        1. nin-jin
          21.04.2023 17:14

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

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

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

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

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

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


          1. iOneM
            21.04.2023 17:14

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

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

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

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

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

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

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


        1. strannik_k
          21.04.2023 17:14
          +1

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

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

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

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

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

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

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


          1. iOneM
            21.04.2023 17:14

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

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

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


  1. Daseron
    21.04.2023 17:14
    +1

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


  1. iOneM
    21.04.2023 17:14
    +1

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

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

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


  1. mogilevtsevdmitry
    21.04.2023 17:14

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


  1. Zimtir
    21.04.2023 17:14

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

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

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

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

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

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

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

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

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

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

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

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

    Привет, Event Loop