Skip to main content

React Hooks

Хуки позволяют функциональным компонентам React иметь состояние (state) и методы жизненного цикла (lifecycle methods) подобно классовым компонентам. Появление хуков привело к тому в настоящее время классовые компоненты в React почти не используются.

useState

Хук useState() предназначен для управления состоянием компонента. Данная функция возвращает пару геттер/сеттер - значение начального состояния и функцию для обновления этого значения. Функцию имеет следующую сигнатуру: const [value, setValue] = useState(defaultValue).

Обновление одного состояния

const UpdateState = () => {
const [age, setAge] = useState(19)

const handleClick = () => setAge(age + 1)

return (
<>
<p>Мне {age} лет.</p>
<button onClick={handleClick}>Стать старше!</button>
</>
)
}

Обновление нескольких состояний

const MultipleStates = () => {
const [age, setAge] = useState(19)
const [num, setNum] = useState(1)

const handleAge = () => setAge(age + 1)
const handleNum = () => setNum(num + 1)

return (
<>
<p>Мне {age} лет.</p>
<p>У меня {num} братьев и сестер.</p>
<button onClick={handleAge}>Стать старше!</button>
<button onClick={handleNum}>Больше братьев и сестер!</button>
</>
)
}

Обновление объекта состояния

const StateObject = () => {
const [state, setState] = useState({ age: 19, num: 1 })

const handleClick = (val) =>
setState({
...state,
[val]: state[val] + 1
})

const { age, num } = state
return (
<>
<p>Мне {age} лет.</p>
<p>У меня {num} братьев и сестер.</p>
<button onClick={() => handleClick('age')}>Стать старше!</button>
<button onClick={() => handleClick('num')}>
Больше братьев и сестер!
</button>
</>
)
}

Счетчик

const CounterState = () => {
const [count, setCount] = useState(0)

return (
<>
<p>Значение счетчика: {count}.</p>
<button onClick={() => setCount(0)}>Сбросить</button>
<button onClick={() => setCount((prevVal) => prevVal + 1)}>
Увеличить (+)
</button>
<button onClick={() => setCount((prevVal) => prevVal - 1)}>
Уменьшить (-)
</button>
</>
)
}

Более сложный пример

const Profile = () => {
const [profile, setProfile] = useState({
firstName: 'Иван',
lastName: 'Петров'
})

const handleChange = ({ target: { value, name } }) => {
setProfile({ ...profile, [name]: value })
}

const { firstName, lastName } = profile
return (
<>
<h1>Профиль</h1>
<form>
<input
type='text'
value={firstName}
onChange={handleChange}
name='firstName'
/> <br />
<input
type='text'
value={lastName}
onChange={handleChange}
name='lastName'
/>
</form>
<p>
Имя: {firstName} <br />
Фамилия: {lastName}
</p>
</>
)
}

useEffect

Хук useEffect() предназначен для запуска побочных эффектов (например, выполнение сетевого запроса или добавление обработчика событий) после монтирования и отрисовки компонента. Данная функция принимает колбек и массив зависимостей. Что касается массива зависимостей, то логика следующая:

  • массив не указан: эффект запускается при каждом рендеринге
  • указан пустой массив: эффект запускается только один раз
  • указан массив с элементами: эффект запускается при изменении любого элемента

Очистка эффектов производится посредством возвращения значений из хука.

Функция имеет следующую сигнатуру:

// обработчик
const handler = () => {
console.log('Случился клик!')
}

useEffect(() => {
// запуск эффекта
window.addEventListener('click', handler)

// очистка эффекта
return () => {
window.removeEventListener('click', handler)
}
// массив зависимостей
}, [handler])

Базовый пример

const BasicEffect = () => {
const [age, setAge] = useState(19)

const handleClick = () => setAge(age + 1)

useEffect(() => {
document.title = `Тебе ${age} лет!`
})

return (
<>
<p>Обратите внимание на заголовок текущей вкладки браузера.</p>
<button onClick={handleClick}>Обновить заголовок!</button>
</>
)
}

Очистка эффекта

const CleanupEffect = () => {
useEffect(() => {
const clicked = () => console.log('Клик!')

window.addEventListener('click', clicked)

return () => {
window.removeEventListener('click', clicked)
}
}, [])

return (
<>
<p>После клика по области просмотра в консоли появится сообщение.</p>
</>
)
}

Несколько эффектов

const MultipleEffects = () => {
useEffect(() => {
const clicked = () => console.log('Клик!')

window.addEventListener('click', clicked)

return () => {
window.removeEventListener('click', clicked)
}
}, [])

useEffect(() => {
console.log('Второй эффект.')
})

return (
<>
<p>Загляните в консоль.</p>
</>
)
}

Массив зависимостей

const DependencyEffect = () => {
const [randomInt, setRandomInt] = useState(0)
const [effectLogs, setEffectLogs] = useState([])
const [count, setCount] = useState(1)

useEffect(() => {
setEffectLogs((prev) => [
...prev,
`Вызов функции номер ${count}.`
])

setCount(count + 1)
}, [randomInt])

return (
<>
<h3>{randomInt}</h3>
<button onClick={() => setRandomInt(~~(Math.random() * 10))}>
Получить случайное целое число!
</button>
<ul>
{effectLogs.map((effect, i) => (
<li key={i}>{' 😈 '.repeat(i) + effect}</li>
))}
</ul>
</>
)
}

Пропуск эффекта

const SkipEffect = () => {
const [randomInt, setRandomInt] = useState(0)
const [effectLogs, setEffectLogs] = useState([])
const [count, setCount] = useState(1)

useEffect(() => {
setEffectLogs((prev) => [
...prev,
`Вызов функции номер ${count}.`
])

setCount(count + 1)
}, [])

return (
<>
<h3>{randomInt}</h3>
<button onClick={() => setRandomInt(~~(Math.random() * 10))}>
Получить случайное целое число!
</button>
<ul>
{effectLogs.map((effect, i) => (
<li key={i}>{' 😈 '.repeat(i) + effect}</li>
))}
</ul>
</>
)
}

Более сложный пример

const UserList = () => {
const [users, setUsers] = useState([])
const [count, setCount] = useState(1)
const [url, setUrl] = useState(
`https://jsonplaceholder.typicode.com/users?_limit=${count}`
)

useEffect(() => {
async function fetchUsers() {
const response = await fetch(url)
const users = await response.json()
setUsers(users)
}
fetchUsers()
}, [url])

const handleSubmit = (e) => {
e.preventDefault()
setUrl(`https://jsonplaceholder.typicode.com/users?_limit=${count}`)
}

return (
<>
<h1>Пользователи</h1>
<form onSubmit={handleSubmit}>
<label>
Сколько пользователей вы хотите получить?
<input
type='number'
value={count}
onChange={(e) => {
setCount(e.target.value)
}}
/>
<button>Получить</button>
</label>
</form>
<ul>
{users.map(({ id, name, username, email, address: { city } }) => (
<li key={id}>
<span>
<b>Имя:</b> {name}
</span>
<br />
<span>
<i>Имя пользователя:</i> {username}
</span>
<br />
<span>
<b>Адрес электронной почты:</b> {email}
</span>
<br />
<span>
<i>Город:</i> {city}
</span>
<br />
</li>
))}
</ul>
</>
)
}

useLayoutEffect

Хук useLayoutEffect() похож на хук useEffect(), за исключением того, что он запускает эффект перед отрисовкой компонента. Данный хук предназначен для запуска эффектов, влияющих на внешний вид DOM, незаметно для пользователя. Эта функция имеет такую же сигнатуру, что и useEffect(). В подавляющем большинстве случаев для запуска побочных эффектов используется useEffect().

Базовый пример

const LayoutEffect = () => {
const [randomInt, setRandomInt] = useState(0)
const [effectLogs, setEffectLogs] = useState([])
const [count, setCount] = useState(1)

useLayoutEffect(() => {
setEffectLogs((prev) => [...prev, `Вызов функции номер ${count}.`])

setCount(count + 1)
}, [randomInt])

return (
<>
<h3>{randomInt}</h3>
<button onClick={() => setRandomInt(~~(Math.random() * 10))}>
Получить случайное целое число!
</button>
<ul>
{effectLogs.map((effect, i) => (
<li key={i}>{' 😈 '.repeat(i) + effect}</li>
))}
</ul>
</>
)
}

useContext

Хук useContext() предназначен для прямой передачи пропов компонентам, находящимся на любом уровне вложенности. Он позволяет избежать так называемого "бурения пропов" (prop drilling), т.е. необходимости последовательной передачи пропов на каждом уровне вложенности.

Создание контекста:

import { createContext } from 'react'

export const ContextName = createContext()

Передача значения контекста нижележащим компонентам:

<ContextName.Provider value={initialValue}>
<App />
</ContextName.Provider>

Получение значения контекста:

import { useContext } from 'react'
import { ContextName } from './ContextName'

const contextValue = useContext(ContextName)

Базовый пример

const ChangeTheme = () => {
const [mode, setMode] = useState('light')

const handleClick = () => {
setMode(mode === 'light' ? 'dark' : 'light')
}

const ThemeContext = createContext(mode)

const theme = useContext(ThemeContext)

return (
<div
style={{
background: theme === 'light' ? '#eee' : '#222',
color: theme === 'light' ? '#222' : '#eee',
display: 'grid',
placeItems: 'center',
minWidth: '320px',
minHeight: '320px',
borderRadius: '4px'
}}
>
<p>Выбранная тема: {theme}.</p>
<button onClick={handleClick}>Поменять тему оформления</button>
</div>
)
}

Более сложный пример

// TodoContext.js
import { createContext, useState } from 'react'

export const TodoContext = createContext()

export const TodoProvider = ({ children }) => {
const [todos, setTodos] = useState([])

return (
<TodoContext.Provider value={[todos, setTodos]}>
{children}
</TodoContext.Provider>
)
}
// index.js
import React, { StrictMode, render } from 'react'

import { Form } from './Form'
import { List } from './List'

import { TodoProvider } from './TodoContext'

const root = document.getElementById('root')
render(
<StrictMode>
<TodoProvider>
<Form />
<List />
</TodoProvider>
</StrictMode>,
root
)
// Form.js
import { useState, useContext } from 'react'
import { TodoContext } from './TodoContext'

export const Form = () => {
// локальное состояние
const [text, setText] = useState('')
// глобальное состояние
const [todos, setTodos] = useContext(TodoContext)

const handleChange = ({ target: { value } }) => {
setText(value)
}

const handleSubmit = (e) => {
e.preventDefault()

const newTodo = {
id: Date.now(),
text
}

setTodos([...todos, newTodo])

setText('')
}

return (
<form onSubmit={handleSubmit}>
<label>
Текст задачи: <br />
<input
type='text'
value={text}
onChange={handleChange}
/> <br />
<button>Добавить</button>
</label>
</form>
)
}
// List.js
import { useContext } from 'react'
import { TodoContext } from './TodoContext'

export const List = () => {
// глобальное состояние
const [todos] = useContext(TodoContext)

return (
<ul>
{todos.map(({ id, text }) => <li key={id}>{text}</li>)}
</ul>
)
}

useReducer

Хук useReducer(), как и хук useState(), предназначен для управления состоянием. Он используется при наличии сложной логики управления состоянием или когда следующее состояние зависит от предыдущего. useReducer() принимает редуктор (reducer), обновляющий состояние на основе типа (type) и, опционально, полезной нагрузки (payload) переданной операции (action).

Сигнатура редуктора:

const reducer = (state, action) => {
switch(action.type) {
case 'actionType':
return newState // { value: state.value + action.payload }
default:
return state
}
}

Использование хука:

const [state, dispatch] = useReducer(reducer, initialState, initFn)
// dispatch({ type: 'actionType', payload: 'actionPayload' }) используется для отправки операции в редуктор (для обновления состояния)
// initFn - функция для "ленивой" установки начального состояния

Базовый пример

// начальное состояние
const initialState = { width: 30 }

// редуктор
const reducer = (state, action) => {
switch (action) {
case 'plus':
return { width: Math.min(state.width + 30, 600) }
case 'minus':
return { width: Math.max(state.width - 30, 30) }
default:
throw new Error('Что происходит?')
}
}

const BasicReducer = () => {
const [state, dispatch] = useReducer(reducer, initialState)

const [color, setColor] = useState('#f0f0f0')

useEffect(() => {
const randomColor = `#${((Math.random() * 0xfff) << 0).toString(16)}`
setColor(randomColor)
}, [state])

return (
<>
<div
style={{
margin: '0 auto',
background: color,
height: '100px',
width: state.width
}}
></div>
<button onClick={() => dispatch('plus')}>
Увеличить ширину контейнера.
</button>
<button onClick={() => dispatch('minus')}>
Уменьшить ширину контейнера.
</button>
</>
)
}

Отложенная установка начального состояния

const initializeState = () => ({
width: 90
})

// обратите внимание как `initializeState()` перезаписывает начальное значение
const initialState = { width: 0 }

const reducer = (state, action) => {
switch (action) {
case 'plus':
return { width: Math.min(state.width + 30, 600) }
case 'minus':
return { width: Math.max(state.width - 30, 30) }
default:
throw new Error('Что происходит?')
}
}

const LazyState = () => {
const [state, dispatch] = useReducer(reducer, initialState, initializeState)

const [color, setColor] = useState('#f0f0f0')

useEffect(() => {
const randomColor = `#${((Math.random() * 0xfff) << 0).toString(16)}`
setColor(randomColor)
}, [state])

return (
<>
<div
style={{
margin: '0 auto',
background: color,
height: '100px',
width: state.width
}}
></div>
<button onClick={() => dispatch('plus')}>
Увеличить ширину контейнера.
</button>
<button onClick={() => dispatch('minus')}>
Уменьшить ширину контейнера.
</button>
</>
)
}

Установка нового состояния

const NewState = () => {
const [state, setState] = useReducer(reducer, initialState)

const [color, setColor] = useState('#f0f0f0')

useEffect(() => {
const randomColor = `#${((Math.random() * 0xfff) << 0).toString(16)}`
setColor(randomColor)
}, [state])

return (
<>
<div
style={{
margin: '0 auto',
background: color,
height: '100px',
width: state.width
}}
></div>
<button onClick={() => setState({ width: 300 })}>
Увеличить ширину контейнера.
</button>
<button onClick={() => setState({ width: 30 })}>
Уменьшить ширину контейнера.
</button>
</>
)
}

useReducer + useContext

// TodoReducer.js
export const initialState = {
todos: [
{ id: 1, text: 'Изучить React' },
{ id: 2, text: 'Изучить Redux' },
{ id: 3, text: 'Изучить GraphQL' }
]
}

export const reducer = (state, action) => {
switch (action.type) {
case 'add':
return {
todos: [...state.todos, action.payload]
}
case 'remove':
return {
todos: state.todos.filter((todo) => todo.id !== action.payload)
}
default:
throw new Error('Что происходит?')
}
}
// TodoContext.js
import { createContext } from 'react'

import { reducer, initialState } from './TodoReducer'

export const TodoContext = createContext()

export const TodoProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)

return (
<TodoContext.Provider value={[state, dispatch]}>
{children}
</TodoContext.Provider>
)
}
// index.js
import React, { StrictMode, render } from 'react'

import { Form } from './Form'
import { List } from './List'

import { TodoProvider } from './TodoContext'

const root = document.getElementById('root')
render(
<StrictMode>
<TodoProvider>
<Form />
<List />
</TodoProvider>
</StrictMode>,
root
)
// Form.js
import { useState, useContext } from 'react'

import { TodoContext } from './TodoContext'

export const Form = () => {
const [text, setText] = useState('')
// замыкающая запятая (trailing comma)
const [, dispatch] = useContext(TodoContext)

const handleChange = ({ target: { value } }) => {
setText(value)
}

const handleSubmit = (e) => {
e.preventDefault()

const newTodo = {
id: Date.now(),
text
}

dispatch({ type: 'add', payload: newTodo })

setText('')
}

return (
<form onSubmit={handleSubmit}>
<label>
Текст задачи: <br />
<input type='text' value={text} onChange={handleChange} /> <br />
<button>Добавить</button>
</label>
</form>
)
}
// List.js
import { useContext } from 'react'

import { TodoContext } from './TodoContext'

export const List = () => {
const [state, dispatch] = useContext(TodoContext)

const handleClick = (id) => {
dispatch({ type: 'remove', payload: id })
}

return (
<ul>
{state.todos.map(({ id, text }) => (
<li key={id}>
<span>{text}</span>
<button onClick={() => handleClick(id)}>Удалить</button>
</li>
))}
</ul>
)
}

useCallback

Хук useCallback() возвращает мемоизированную версию переданной функции обратного вызова. Данный хук принимает колбек и массив зависимостей. колбек повторно вычисляется только при изменении значений одной из зависимостей. Хук имеет следующую сигнатуру:

useCallback(
fn,
[deps]
) // deps - dependencies, зависимости

Базовый пример

const BasicCallback = () => {
const [age, setAge] = useState(19)

const handleClick = () => { setAge(age < 100 ? age + 1 : age) }

const getRandomColor = useCallback(
() => `#${((Math.random() * 0xfff) << 0).toString(16)}`,
[]
)

return (
<>
<Age age={age} handleClick={handleClick} />
<Guide getRandomColor={getRandomColor} />
</>
)
}

const Age = ({ age, handleClick }) => {
return (
<div>
<p>Мне {age} лет.</p>
<p>Нажми на кнопку 👇</p>
<button onClick={handleClick}>Стать старше!</button>
</div>
)
}

// `React.memo()` позволяет зафиксировать состояние компонента
const Guide = memo(({ getRandomColor }) => {
const color = getRandomColor()

return (
<div style={{ background: color, padding: '.4rem' }}>
<p style={{ color: color, filter: 'invert()' }}>
Следуй инструкциям максимально точно.
</p>
</div>
)
})

Зависимый колбек

const DependencyCallback = () => {
const [age, setAge] = useState(19)

const handleClick = () => { setAge(age < 100 ? age + 1 : age) }

const getRandomColor = useCallback(
() => `#${((Math.random() * 0xfff) << 0).toString(16)}`,
[age]
)

return (
<>
<Age age={age} handleClick={handleClick} />
<Guide getRandomColor={getRandomColor} />
</>
)
}

const Age = ({ age, handleClick }) => {
return (
<div>
<p>Мне {age} лет.</p>
<p>Нажми на кнопку 👇</p>
<button onClick={handleClick}>Стать старше!</button>
</div>
)
}

const Guide = memo(({ getRandomColor }) => {
const color = getRandomColor()

return (
<div style={{ background: color, padding: '.4rem' }}>
<p style={{ color: color, filter: 'invert()' }}>
Следуй инструкциям максимально точно.
</p>
</div>
)
})

Более сложный пример

// пользовательский хук для добавления обработчиков событий
const useEventListener = (ev, cb, $ = window) => {
const cbRef = useRef()

useEffect(() => {
cbRef.current = cb
}, [cb])

useEffect(() => {
const listener = (ev) => cbRef.current(ev)

$.addEventListener(ev, listener)

return () => {
$.removeEventListener(ev, listener)
}
}, [ev, $])
}

const CoordsCallback = () => {
const [coords, setCoords] = useState({ x: 0, y: 0 })

const cb = useCallback(
({ clientX, clientY }) => {
setCoords({ x: clientX, y: clientY })
},
[setCoords]
)

useEventListener('mousemove', cb)

const { x, y } = coords

return (
<h1>
Координаты курсора мыши: {x}, {y}
</h1>
)
}

useMemo

Хук useMemo() является альтернативой хука useCallback(), но принимает любые значения, а не только функции. Данный хук имеет следующую сигнатуру:

useMemo(() => {
fn,
[deps]
}) // deps - зависимости

Базовый пример

const BasicMemo = () => {
const [age, setAge] = useState(19)

const handleClick = () => { setAge(age < 100 ? age + 1 : age) }

const getRandomColor = () => `#${((Math.random() * 0xfff) << 0).toString(16)}`

const memoizedGetRandomColor = useMemo(() => getRandomColor, [])

return (
<>
<Age age={age} handleClick={handleClick} />
<Guide getRandomColor={memoizedGetRandomColor} />
</>
)
}

const Age = ({ age, handleClick }) => {
return (
<div>
<p>Мне {age} лет.</p>
<p>Нажми на кнопку 👇</p>
<button onClick={handleClick}>Стать старше!</button>
</div>
)
}

const Guide = memo(({ getRandomColor }) => {
const color = getRandomColor()

return (
<div style={{ background: color, padding: '.4rem' }}>
<p style={{ color: color, filter: 'invert()' }}>
Следуй инструкциям максимально точно.
</p>
</div>
)
})

Зависимая мемоизация

const DependencyMemo = () => {
const [age, setAge] = useState(19)

const handleClick = () => { setAge(age < 100 ? age + 1 : age) }

const getRandomColor = () => `#${((Math.random() * 0xfff) << 0).toString(16)}`

const memoizedGetRandomColor = useMemo(() => getRandomColor, [age])

return (
<>
<Age age={age} handleClick={handleClick} />
<Guide getRandomColor={memoizedGetRandomColor} />
</>
)
}

const Age = ({ age, handleClick }) => {
return (
<div>
<p>Мне {age} лет.</p>
<p>Нажми на кнопку 👇</p>
<button onClick={handleClick}>Стать старше!</button>
</div>
)
}

const Guide = memo(({ getRandomColor }) => {
const color = getRandomColor()

return (
<div style={{ background: color, padding: '.4rem' }}>
<p style={{ color: color, filter: 'invert()' }}>
Следуй инструкциям максимально точно.
</p>
</div>
)
})

Более сложный пример

const WordsMemo = () => {
const [count, setCount] = useState(0)
const [index, setIndex] = useState(0)

const words = ['hi', 'programming', 'bye', 'real', 'world']
const word = words[index]

const getLetterCount = (word) => {
let i = 0
while (i < 1e9) i++
return word.length
}

const memoized = useMemo(() => getLetterCount(word), [word])

return (
<div style={{ padding: '15px' }}>
<h2>Вычисление количества букв (медленно 🐌)</h2>
<p>
В слове "{word}" {memoized} букв
</p>
<button
onClick={() => {
const next = index + 1 === words.length ? 0 : index + 1
setIndex(next)
}}
>
Следующее слово
</button>

<h2>Увеличение значения счетчика (быстро ⚡️)</h2>
<p>Значение счетчика: {count}</p>
<button onClick={() => setCount(count + 1)}>Увеличить</button>
</div>
)
}

useRef

Хук useRef() возвращает объект, свойство current которого содержит ссылку на узел DOM. Данный хук также может использоваться для сохранения любого мутирующего значения.

Создание хука: const node = useRef().

Добавление ссылки: <tagName ref={node}></tagName>.

Получение доступа к DOM-элементу

const DomAccess = () => {
const textareaEl = useRef(null)

const handleClick = () => {
textareaEl.current.value = 'Изучай хуки внимательно! Они не так просты, как кажется'
textareaEl.current.focus()
}

return (
<>
<button onClick={handleClick}>Получить сообщение.</button>
<label htmlFor='message'>
После нажатия кнопки в поле для ввода текста появится сообщение.
</label>
<textarea ref={textareaEl} id='message' />
</>
)
}

Запуск и остановка таймера

const IntervalRef = () => {
const [time, setTime] = useState(0)
const interval = useRef()

useEffect(() => {
const id = setInterval(() => {
setTime((time) => (time = new Date().toLocaleTimeString()))
}, 1000)

interval.current = id

return () => clearInterval(interval.current)
}, [time])

return (
<>
<p>Текущее время:</p>
<time>{time}</time>
</>
)
}

Несколько ссылок

const StringVal = () => {
const textareaEl = useRef(null)
const stringVal = useRef('Изучай хуки внимательно! Они не так просты, как кажется')

const handleClick = () => {
textareaEl.current.value = stringVal.current
textareaEl.current.focus()
}

return (
<>
<button onClick={handleClick}>Получить сообщение.</button>
<label htmlFor='message'>
После нажатия кнопки в поле для ввода текста появится сообщение.
</label>
<textarea ref={textareaEl} id='message' />
</>
)
}

Более сложный пример

const ProfileRef = () => {
const firstNameInput = useRef(null)
const lastNameInput = useRef(null)

const [profile, setProfile] = useState({})

function handleSubmit(e) {
e.preventDefault()
setProfile({
firstName: firstNameInput.current.value,
lastName: lastNameInput.current.value
})
}

return (
<>
<h1>Профиль</h1>
<form onSubmit={handleSubmit}>
<input type='text' ref={firstNameInput} name='fistName' /> <br />
<input type='text' ref={lastNameInput} name='lastName' /> <br />
<button>Отправить</button>
<p>
Имя: {profile?.firstName} <br />
Фамилия: {profile?.lastName}
</p>
</form>
</>
)
}