Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Как мы все знаем, в 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, но такова цена производительности.
В целом, разница в производительности незначительна. Но чем сложнее компонент, тем выше разрыв между обычными и afc-компонентами (react-afc может быть до 10% быстрее).
Пакет может измениться в будущем. Переписывать на него существующие проекты не нужно. А вот использовать в новых может быть очень даже удобно.
Жду вашего мнения в комментариях :)