Как мы все знаем, в React есть функциональные и классовые компоненты. Каждый вид имеет свои плюсы и минусы.

Классовые компоненты имеют меньшую производительность по сравнению с функциональными и вызывают некоторые сложности в переиспользовании одинаковой логики.

Моё мнение

Лично мне не нравятся повсеместные стрелочные функции и this.

Функциональные компоненты, в свою очередь, для оптимизации заставляют нас оборачивать объекты в useMemo, а функции в useCallback. Что уменьшает читаемость кода, а при большом количестве вызовов также понижает производительность (как бы это не было парадоксально).

Вы можете задаться вопросом: "Разве у нас есть иной вариант?". Да, он существует!
Что, если взять функциональный компонент и добавить ему функцию "конструктор", подобно одноимённому методу в классах?. Тогда нам не потребуется оборачивать в useMemo и useCallback, так как объекты и функции будут создаваться один раз. Также мы не потеряем удобное переиспользование логики и нам не потребуется this на каждой строке.

Довольно заманчивые условия, но разве это возможно сделать без "костылей"?
Я задался этим вопросом и нашёл решение: использовать замыкания для реализации "конструктора". После нескольких вечеров на просторах интернета "родился" npm-пакет react-afc.

Как мог выглядеть компонент со сложной логикой на чистом React:

import React, { useMemo, useState, useCallback, memo } from 'react'
import ComplexInput from './ComplexInput'
import ComplexOutput from './ComplexOutput'

function Component(props) {
  const [text, setText] = useState('')
  
  const config = useMemo(() => ({
    showDesc: true,
    title: 'Title'
  }), [])

  const onChangeText = useCallback(e => {
    setText(e.target.value)
  })

  const onBlur = useCallback(() => {
    // hard calculations
  })

  return <>
    <ComplexInput value={text} onChange={onChangeText} onBlur={onBlur} />
    <ComplexOutput config={config} />
  </>
}

export default memo(Component)

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

Тот же пример, но с использованием react-afc:

import React from 'react'
import { afcMemo, useState } from 'react-afc'
import ComplexInput from './ComplexInput'
import ComplexOutput from './ComplexOutput'

function Component(props) {
  const [text, setText] = useState('')
  
  const config = {
    showDesc: true,
    title: 'Title'
  }

  function onChangeText(e) {
    setText(e.target.value)
  }

  function onBlur() {
    // hard calculations
  }

  return () => <>
    <ComplexInput value={text.val} onChange={onChangeValue} onBlur={onBlur} />
    <ComplexOutput config={config} />
  </>
}

export default afcMemo(Component)

Что же изменилось?

Теперь функция компонент является "конструктором" и вызывается только один раз в течение всего жизненного цикла компонента. Это значит, что onChangeText, onBlur и config одинаковые каждый рендер (без использования обёрток), то есть они не вызывают перерисовку "детей" при обновлении компонента. Конструктор возвращает рендер-функцию, которая вызывается каждый рендер.

Что насчёт производительности?

Пакет максимально переиспользует React-хуки: при нескольких вызовах useState из react-afc используется лишь один хук из React. Это ломает просмотр состояний компонентов в ReactDevtools, но такова цена производительности (исправлено начиная с v3.3.0).

В целом, разница в производительности незначительна. Но чем сложнее компонент, тем выше разрыв между обычными и afc-компонентами (react-afc может быть до 10% быстрее).

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

Жду вашего мнения в комментариях :)

Комментарии (13)


  1. funca
    21.01.2023 01:49
    +1

    Какой-то такой этап, реакт уже проходил года 4 или 5 тому назад, то есть где-то между эпохами классов и хуков.

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


  1. VerZsuT Автор
    21.01.2023 11:57
    +2

    Уже исправляю видимость состояния в ReactDevtools и показ названия компонента в древе


  1. NIMAN
    21.01.2023 11:57

    Это ломает просмотр состояний компонентов в ReactDevtools, но такова цена производительности. - дальше не стал читать


  1. elyorweb
    21.01.2023 11:57
    +2

    А что мешает взять какой нибудь стм (Mobx) и там все это дело написать?


    1. markelov69
      21.01.2023 12:12
      +3

      А что мешает взять какой нибудь стм (Mobx) и там все это дело написать?

      Ничего не мешает, но тогда для любителей страдать и находить для себя "проблемы" и "решать" их всё станет слишком скучно и не интересно. Просто пишешь и всё работает прекрасно - кому из извращенцев такое понравится?)


      1. elyorweb
        21.01.2023 19:41

        Создадим себе проблемы, и сами же их решим. Как же скучная жизнь без этих проблем((


  1. DmitryKazakov8
    21.01.2023 20:34
    +3

    "Лично мне не нравятся повсеместные стрелочные функции и this" - странный аргумент для меня. Написание method = () => вместо method() и this сподвигли на то, чтобы выпустить новую утилиту, которая превращает функциональные компоненты Реакта в SolidJS-like? Но классы ведь придуманы для удобной организации кода. Чтобы не была функция из 5000 строк и 50 вложенных функций смешанного назначения, а были удобно организованные слои - хендлеры пользовательский событий, реакции, состояние, геттеры подготовленных данных, которые схлопываются IDE и гармонично могут использовать друг друга без заботы о hoisting, когда const a = 1 объявлена ниже, чем используется.

    И в целом функциональные компоненты для меня очень неудобны как раз отсутствием this - в каждый хук надо передавать состояние, результат других хуков и при необходимости ссылки на них, в данной статье эти кейсы не показаны, но в реальности может быть myCustomHook(...10 params). В классах этой проблемы нет благодаря this и тому, что каждый метод имеет доступ ко всем другим методам, геттерам и состоянию.

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


    1. VerZsuT Автор
      21.01.2023 21:04
      +2

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


      1. funca
        22.01.2023 00:07

        Чтобы понять логику разработчиков, надо смотреть языки где есть поддержка функционального программирования - не такая ущербная как в JS, но и не такая упоротая как в Haskell. Например OCaml.

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

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


  1. andres_kovalev
    22.01.2023 16:11

    А как быть со случаями, когда пропы компонента меняются и коллбек должен об этом знать? Создавать в замыкании переменные, через которые прокидывать пропы?


    1. VerZsuT Автор
      23.01.2023 00:35

      Объектprops автоматически обновляется каждый рендер (не передаётся новый объект, а изменяются его свойства)


  1. asnow
    23.01.2023 09:07

    Не пойму я никак чем это все отличается от

    var el = <Component props={any}

    Или React.memo

    Или какой там уровня кэша...

    На сам деле не понятно что за проблема у вас


    1. VerZsuT Автор
      23.01.2023 11:02

      Если в переиспользуемом компоненте есть сложная логика, то решая проблему оптимизации без доп. инструментов всё придёт к вездесущим memo, useMemo, useCallback или настолько большой декомпозиции, что и сам ногу сломишь, и React будет не рад такому количеству нод.

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

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