react-afc - библиотека для более простого (чем в простом react) уменьшения количества ненужных ререндеров дочерних компонентов.
Задачи и применение
В обычном react функциональный компонент вызывается каждый раз когда изменяется его состояние или пропсы, что вызывает повторное создание всех callback'ов и переменных.
Так как передаваемые данные из предыдущего и текущего рендера не равны, это порождает ререндер дочерних компонентов.
пример
Функционал компонента не несёт конкретного смысла. Просто пример.
import { useState } from 'react' import Title from 'title-lib' import HardCalcHeader from './header' import NameInput from './name-input' import AgeInput from './age-input' function App() { const [name, setName] = useState('') const [age, setAge] = useState(1) const onChangeName = value => setName(value) const onChangeAge = value => setAge(value) const closeWindow = () => window.close() const titleArgs = { color: 'blue', size: 20 } return ( <> <Title text='Amazing app' args={titleArgs} /> <HardCalcHeader onExit={closeWindow} /> <NameInput value={name} onChange={onChangeName} /> <AgeInput value={age} onChange={onChangeAge} /> </> ) }
При изменении имени происходит перерисовка Title, NameInput, AgeInput, а также HardCalcHeader, что приводит к зависанию приложения.
Для избежания этого поведения мы разбиваем логику на множество компонентов (не всегда удобно), либо используем useCallback и useMemo (стоит дополнительных затрат при многократном вызове, ухудшает читаемость кода и требует отслеживания зависимостей функции).
пример с использованием хуков
import { useState, useCallback, useMemo } from 'react' import Title from 'title-lib' import HardCalcHeader from './header' import NameInput from './name-input' import AgeInput from './age-input' function App() { const [name, setName] = useState('') const [age, setAge] = useState(1) const onChangeName = useCallback(value => setName(value), []) const onChangeAge = useCallback(value => setAge(value), []) const closeWindow = useCallback(() => window.close(), []) const titleArgs = useMemo(() => ({ color: 'blue', size: 20 }), []) return ( <> <Title text='Amazing app' args={titleArgs} /> <HardCalcHeader onExit={closeWindow} /> <NameInput value={name} onChange={onChangeName} /> <AgeInput value={age} onChange={onChangeAge} /> </> ) }
Вся суть работы библиотеки лежит в одном, но значительном изменении структуры функционального компонента - добавлении аналога конструктора классового компонента.
сравнение
import { afc } from 'react-afc' // обычный компонент function CommonComponent(props) { // вызывается каждый рендер // ...react-хуки return <p>обычный компонент</p> } // afc компонент const AdvancedComponent = afc(props => { // вызывается один раз за весь жизненный цикл компонента // afc-хуки return () => { // render-функция, вызывается каждый рендер // ...react-хуки (только по необходимости) return <p>afc</p> } }
Данное изменение позволяет нам во многих случаях не использовать useCallback и useMemo, а также не думать о зависимостях.
��ример с использованием библиотеки
import { afc, useState } from 'react-afc' import Title from 'title-lib' import HardCalcHeader from './header' import NameInput from './name-input' import AgeInput from './age-input' const App = afc(() => { const [getName, setName] = useState('') const [getAge, setAge] = useState(1) const onChangeName = value => setName(value) const onChangeAge = value => setAge(value) const closeWindow = () => window.close() const titleArgs = { color: 'blue', size: 20 } return () => ( <> <Title text='Amazing app' args={titleArgs} /> <HardCalcHeader onExit={closeWindow} /> <NameInput value={getName()} onChange={onChangeName} /> <AgeInput value={getAge()} onChange={onChangeAge} /> </> ) })
Работает аналогично примеру с хуками, никаких лишних перерисовок.
Побочным эффектом является то, что мы больше не нуждаемся в использовании useRef для передачи данных между рендерами.
пример
import { useRef } from 'react' import { afc } from 'react-afc' // обычный компонент function CommonComponent() { const renderCount = useRef(0) renderCount.current++ return ( <p> Рендер вызван {renderCount.current} раз </p> ) } // afc компонент const AdvancedComponent = afc(() => { let renderCount = 0 return () => { renderCount++ return ( <p> Рендер вызван {renderCount} раз </p> ) } })
Примечание: в библиотеке имеются аналоги для useState, useRef, useMemo, useEffect, memo. Их применение узкоспециализировано, читайте доку.
Пример работы можете найти на codesandbox.
Принцип работы
При первом рендере библиотека вызывает переданную в afc функцию, определяет какие хуки используются и сохраняет возвращённую рендер-функцию.
При последующих рендерах обновляются свойства в объекте пропсов (если они изменились), выполняются определённые ранее react-хуки и вызывается рендер-функция.
Принцип прост как пробка и требует незначительных вычислений только при первом рендере.
