Pull to refresh

Что такое react-afc

Level of difficultyMedium
Reading time3 min
Views9.6K

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-хуки и вызывается рендер-функция.

Принцип прост как пробка и требует незначительных вычислений только при первом рендере.

Tags:
Hubs:
Total votes 8: ↑8 and ↓0+8
Comments27

Articles