Средний 12 мин чтения
Хуки React
Хуки позволяют использовать состояние и другие функции React в функциональных компонентах. useState, useEffect, useCallback, useMemo и другие.
hooksuseStateuseEffectuseCallbackuseMemoReact
useState
Добавляет локальное состояние в функциональный компонент:
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Счётчик: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(c => c - 1)}>-</button>
</div>
)
}
Функциональное обновление
// ✅ Используйте функцию для получения актуального состояния
setCount(prev => prev + 1)
// ❌ Может привести к race conditions при нескольких обновлениях
setCount(count + 1)
Объект состояния
const [user, setUser] = useState({ name: '', email: '' })
// Обновление — нужно разворачивать предыдущее состояние
setUser(prev => ({ ...prev, name: 'Иван' }))
useEffect
Выполняет побочные эффекты: запросы, подписки, изменение DOM:
import { useState, useEffect } from 'react'
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
// Эффект запускается после рендера
async function fetchUser() {
const res = await fetch(`/api/users/${userId}`)
setUser(await res.json())
}
fetchUser()
// Функция очистки — запускается перед следующим эффектом или при размонтировании
return () => {
// Отмена запроса если нужно
}
}, [userId]) // Массив зависимостей — эффект запускается при их изменении
return user ? <div>{user.name}</div> : <div>Загрузка...</div>
}
Варианты массива зависимостей
useEffect(() => { /* ... */ }) // Каждый рендер
useEffect(() => { /* ... */ }, []) // Только при монтировании
useEffect(() => { /* ... */ }, [id]) // При изменении id
useCallback
Мемоизирует функцию, предотвращая её пересоздание при каждом рендере:
import { useState, useCallback } from 'react'
function Parent() {
const [count, setCount] = useState(0)
const [items, setItems] = useState([])
// ✅ addItem не пересоздаётся при изменении count
const addItem = useCallback((item) => {
setItems(prev => [...prev, item])
}, []) // Нет зависимостей — функция создаётся один раз
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<ChildList items={items} onAdd={addItem} />
</>
)
}
useMemo
Мемоизирует результат вычисления:
import { useMemo } from 'react'
function ProductList({ products, filter }) {
// Пересчитывается только при изменении products или filter
const filteredProducts = useMemo(
() => products.filter(p => p.category === filter),
[products, filter]
)
return (
<ul>
{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)
}
Правило: использовать useMemo/useCallback только когда это реально нужно — для тяжёлых вычислений или стабильных ссылок для дочерних компонентов с memo.
useRef
Хранит изменяемое значение без ре-рендера:
import { useRef, useEffect } from 'react'
function AutoFocusInput() {
const inputRef = useRef(null)
useEffect(() => {
inputRef.current.focus()
}, [])
return <input ref={inputRef} />
}
// Хранение предыдущего значения
function Timer() {
const timerRef = useRef(null)
function start() {
timerRef.current = setInterval(() => console.log('tick'), 1000)
}
function stop() {
clearInterval(timerRef.current)
}
}
useContext
Подписывается на значение из Context:
import { createContext, useContext, useState } from 'react'
const ThemeContext = createContext('light')
function App() {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={theme}>
<Layout />
</ThemeContext.Provider>
)
}
function ThemedButton() {
const theme = useContext(ThemeContext)
return <button className={`btn--${theme}`}>Кнопка</button>
}
useReducer
Для сложной логики состояния:
import { useReducer } from 'react'
function reducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 }
case 'decrement': return { count: state.count - 1 }
case 'reset': return { count: 0 }
default: throw new Error('Неизвестное действие')
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 })
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Сброс</button>
</>
)
}
Пользовательские хуки
// hooks/useFetch.js
import { useState, useEffect } from 'react'
export function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
async function load() {
try {
const res = await fetch(url)
const json = await res.json()
if (!cancelled) setData(json)
} catch (e) {
if (!cancelled) setError(e)
} finally {
if (!cancelled) setLoading(false)
}
}
load()
return () => { cancelled = true }
}, [url])
return { data, loading, error }
}
// Использование
function Posts() {
const { data: posts, loading, error } = useFetch('/api/posts')
if (loading) return <div>Загрузка...</div>
if (error) return <div>Ошибка: {error.message}</div>
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}