Вивчай

useRef, useMemo, useCallback

React useRef useMemo useCallback — бібліотечна картотека з закладками для швидкого доступуReact useRef useMemo useCallback — бібліотечна картотека з закладками для швидкого доступу

У попередньому уроці ми розібрали useEffect. Тепер познайомимось ще з трьома корисними хуками: useRef для доступу до DOM-елементів, useMemo для мемоізації обчислень та useCallback для мемоізації функцій.


useRef -- посилання на DOM

Іноді потрібен прямий доступ до DOM-елемента: поставити фокус на input, отримати розміри елемента, прокрутити до певної позиції. Для цього є useRef.

import { useRef } from 'react'

function FocusInput() {
  const inputRef = useRef<HTMLInputElement>(null)

  function handleClick() {
    inputRef.current?.focus()
  }

  return (
    <div>
      <input ref={inputRef} placeholder="Натисни кнопку для фокусу" />
      <button onClick={handleClick}>Поставити фокус</button>
    </div>
  )
}

Як це працює

  1. useRef<HTMLInputElement>(null) -- створює об'єкт { current: null }
  2. ref={inputRef} -- React записує DOM-елемент у inputRef.current
  3. inputRef.current?.focus() -- викликаємо метод DOM-елемента

Ще приклади з DOM

function ScrollExample() {
  const bottomRef = useRef<HTMLDivElement>(null)

  function scrollToBottom() {
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
  }

  return (
    <div>
      <button onClick={scrollToBottom}>Прокрутити вниз</button>
      {/* ... багато контенту ... */}
      <div ref={bottomRef}>Кінець сторінки</div>
    </div>
  )
}

useRef для збереження мутабельних значень

useRef також зберігає будь-яке значення, яке зберігається між рендерами, але не викликає перемальовування при зміні:

import { useRef, useEffect } from 'react'

function RenderCounter() {
  const renderCount = useRef(0)

  useEffect(() => {
    renderCount.current += 1
    // Зміна .current НЕ викликає перемальовування
  })

  return <p>Рендерів: {renderCount.current}</p>
}

useRef vs useState

useStateuseRef
Зміна викликає рендерТакНі
Зберігається між рендерамиТакТак
Для чогоUI-дані, що відображаютьсяDOM-елементи, таймери, попередні значення

Типовий приклад: попереднє значення

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T | undefined>(undefined)

  useEffect(() => {
    ref.current = value
  }, [value])

  return ref.current
}

// Використання
function PriceTracker({ price }: { price: number }) {
  const prevPrice = usePrevious(price)

  return (
    <p>
      Ціна: {price} грн
      {prevPrice !== undefined && (
        <span>{price > prevPrice ? ' (зросла)' : ' (знизилась)'}</span>
      )}
    </p>
  )
}

useMemo -- мемоізація обчислень

useMemo запам'ятовує результат "важкого" обчислення і перераховує його тільки коли змінюються залежності:

import { useState, useMemo } from 'react'

function ExpensiveList({ items }: { items: number[] }) {
  const [filter, setFilter] = useState('')
  const [count, setCount] = useState(0)

  // БЕЗ useMemo: сортування виконується при КОЖНОМУ рендері
  // (навіть при зміні count, коли items не змінились)
  // const sortedItems = [...items].sort((a, b) => a - b)

  // З useMemo: сортування виконується тільки при зміні items
  const sortedItems = useMemo(() => {
    console.log('Сортування...')
    return [...items].sort((a, b) => a - b)
  }, [items])

  return (
    <div>
      <button onClick={() => setCount(prev => prev + 1)}>
        Клік: {count}
      </button>
      <ul>
        {sortedItems.map(item => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  )
}

Синтаксис

const memoizedValue = useMemo(() => {
  return expensiveComputation(a, b)
}, [a, b]) // Перераховується тільки при зміні a або b

Ще приклад: фільтрація

interface Product {
  id: number
  name: string
  category: string
  price: number
}

function ProductList({ products }: { products: Product[] }) {
  const [search, setSearch] = useState('')
  const [sortBy, setSortBy] = useState<'name' | 'price'>('name')

  const filteredAndSorted = useMemo(() => {
    return products
      .filter(p => p.name.toLowerCase().includes(search.toLowerCase()))
      .sort((a, b) => {
        if (sortBy === 'price') return a.price - b.price
        return a.name.localeCompare(b.name)
      })
  }, [products, search, sortBy])

  return (
    <div>
      <input
        value={search}
        onChange={(e) => setSearch(e.target.value)}
        placeholder="Пошук..."
      />
      <select value={sortBy} onChange={(e) => setSortBy(e.target.value as 'name' | 'price')}>
        <option value="name">За назвою</option>
        <option value="price">За ціною</option>
      </select>

      <ul>
        {filteredAndSorted.map(p => (
          <li key={p.id}>{p.name} — {p.price} грн</li>
        ))}
      </ul>
    </div>
  )
}

useCallback -- мемоізація функцій

useCallback запам'ятовує функцію і повертає ту саму функцію, поки залежності не зміняться:

import { useState, useCallback } from 'react'

function ParentComponent() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('')

  // Без useCallback: нова функція при кожному рендері
  // const handleClick = () => setCount(prev => prev + 1)

  // З useCallback: та сама функція між рендерами
  const handleClick = useCallback(() => {
    setCount(prev => prev + 1)
  }, [])

  return (
    <div>
      <p>Count: {count}</p>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <ExpensiveChild onClick={handleClick} />
    </div>
  )
}

Навіщо це потрібно?

При кожному рендері компонента всі функції створюються заново. Зазвичай це не проблема. Але якщо ти передаєш функцію як prop дочірньому компоненту, обгорнутому в React.memo, нова функція зламає мемоізацію:

// Дочірній компонент, обгорнутий в memo
const ExpensiveChild = React.memo(function ExpensiveChild({
  onClick,
}: {
  onClick: () => void
}) {
  console.log('ExpensiveChild рендериться')
  return <button onClick={onClick}>Натисни</button>
})

React.memo -- мемоізація компонентів

React.memo запобігає перемальовуванню компонента, якщо його props не змінились:

import { memo } from 'react'

interface UserCardProps {
  name: string
  email: string
}

const UserCard = memo(function UserCard({ name, email }: UserCardProps) {
  console.log('UserCard рендериться')
  return (
    <div>
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  )
})

React.memo порівнює props за посиланням (shallow comparison). Якщо всі props однакові -- компонент не перемальовується.


Коли НЕ потрібна оптимізація

Увага

Передчасна оптимізація -- корінь усіх бід. Не обгортай кожен обчислення в useMemo та кожну функцію в useCallback "на всяк випадок".

useMemo потрібен коли:

  • Обчислення дійсно "важке" (сортування тисяч елементів, складні фільтрації)
  • Результат передається як prop в memo-компонент

useMemo НЕ потрібен коли:

  • Обчислення просте (конкатенація рядків, прості умови)
  • Результат не передається далі

useCallback потрібен коли:

  • Функція передається як prop в React.memo-компонент
  • Функція є залежністю useEffect

useCallback НЕ потрібен коли:

  • Функція використовується тільки локально
  • Дочірній компонент не обгорнутий в memo
Порада

Золоте правило: спочатку напиши код без оптимізацій. Якщо помітиш проблеми з продуктивністю -- тоді використовуй React DevTools Profiler для пошуку вузьких місць та додавай useMemo/useCallback точково.


Підсумок

  • useRef -- доступ до DOM-елементів та збереження мутабельних значень без рендерингу
  • useRef(null) + ref={myRef} -- підключення до DOM
  • useRef зберігає значення між рендерами, але зміна .current не викликає перемальовування
  • useMemo -- мемоізація результату обчислень, перераховується при зміні залежностей
  • useCallback -- мемоізація функцій, корисний в парі з React.memo
  • React.memo -- мемоізація компонента, пропускає рендер якщо props не змінились
  • Не оптимізуй без потреби -- спочатку напиши простий код

Що далі?

У наступному уроці розберемо Context API -- спосіб передачі даних глибоко по дереву компонентів без "пробурювання" props через кожен рівень.

Інфо

Корисні посилання: