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 - функция для "ленивой" установки начального состояния