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>
)
}
Як це працює
useRef<HTMLInputElement>(null)-- створює об'єкт{ current: null }ref={inputRef}-- React записує DOM-елемент уinputRef.currentinputRef.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
useState | useRef | |
|---|---|---|
| Зміна викликає рендер | Так | Ні |
| Зберігається між рендерами | Так | Так |
| Для чого | 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}-- підключення до DOMuseRefзберігає значення між рендерами, але зміна.currentне викликає перемальовування- useMemo -- мемоізація результату обчислень, перераховується при зміні залежностей
- useCallback -- мемоізація функцій, корисний в парі з
React.memo - React.memo -- мемоізація компонента, пропускає рендер якщо props не змінились
- Не оптимізуй без потреби -- спочатку напиши простий код
Що далі?
У наступному уроці розберемо Context API -- спосіб передачі даних глибоко по дереву компонентів без "пробурювання" props через кожен рівень.
Корисні посилання:
- React.dev: Referencing Values with Refs -- useRef
- React.dev: Manipulating the DOM with Refs -- DOM refs
- React.dev: useMemo -- API-довідник
- React.dev: useCallback -- API-довідник