
React Hooks: useState, useEffect, useContext
React Hooks — это функции, которые позволяют использовать состояние и другие возможности React в функциональных компонентах. Hooks были представлены в React 16.8 и стали революционным изменением в подходе к разработке React приложений. Они позволяют писать более чистый, читаемый код и избегать сложностей, связанных с классовыми компонентами. В этой статье рассмотрим три основных хука: useState
, useEffect
и useContext
.
1. Введение в React Hooks
Что такое Hooks?
Hooks — это функции, которые позволяют "подключаться" к состоянию и жизненному циклу React из функциональных компонентов. Они не работают внутри классов — вместо этого они дают возможность использовать React без классов.
Основные правила Hooks:
- Вызывайте Hooks только на верхнем уровне (не внутри циклов, условий или вложенных функций)
- Вызывайте Hooks только из React функциональных компонентов или кастомных Hooks
- Имена Hooks всегда начинаются с
use
Преимущества Hooks
- Более простой код — нет необходимости в
this
и привязке методов - Лучшая читаемость — логика разделяется по функциональности, а не по жизненному циклу
- Переиспользование логики — кастомные Hooks для общей логики
- Меньше boilerplate кода — нет необходимости в конструкторах и методах жизненного цикла
2. useState — управление состоянием
Основы useState
useState
— это хук для добавления состояния в функциональные компоненты:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Счётчик: {count}</p>
<button onClick={() => setCount(count + 1)}>
Увеличить
</button>
<button onClick={() => setCount(count - 1)}>
Уменьшить
</button>
</div>
);
}
Синтаксис useState:
const [state, setState] = useState(initialValue);
state
— текущее значение состоянияsetState
— функция для обновления состоянияinitialValue
— начальное значение состояния
Работа с объектами и массивами
import React, { useState } from 'react';
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
const handleChange = (e) => {
const { name, value } = e.target;
setUser(prevUser => ({
...prevUser,
[name]: value
}));
};
return (
<form>
<input
type="text"
name="name"
value={user.name}
onChange={handleChange}
placeholder="Имя"
/>
<input
type="email"
name="email"
value={user.email}
onChange={handleChange}
placeholder="Email"
/>
<input
type="number"
name="age"
value={user.age}
onChange={handleChange}
placeholder="Возраст"
/>
</form>
);
}
Работа с массивами
import React, { useState } from 'react';
function TodoList() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim()) {
setTodos(prevTodos => [...prevTodos, {
id: Date.now(),
text: inputValue,
completed: false
}]);
setInputValue('');
}
};
const toggleTodo = (id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
};
const deleteTodo = (id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
};
return (
<div>
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Новая задача"
/>
<button onClick={addTodo}>Добавить</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>Удалить</button>
</li>
))}
</ul>
</div>
);
}
Функциональные обновления
Когда новое состояние зависит от предыдущего, используйте функциональную форму:
// ❌ Неправильно — может привести к race conditions
setCount(count + 1);
// ✅ Правильно — гарантирует актуальное значение
setCount(prevCount => prevCount + 1);
3. useEffect — побочные эффекты
Основы useEffect
useEffect
позволяет выполнять побочные эффекты в функциональных компонентах:
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Ошибка загрузки пользователя:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // Зависимости
if (loading) return <div>Загрузка...</div>;
if (!user) return <div>Пользователь не найден</div>;
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
</div>
);
}
Массив зависимостей
// Выполняется после каждого рендера
useEffect(() => {
console.log('Компонент обновился');
});
// Выполняется только при монтировании (аналог componentDidMount)
useEffect(() => {
console.log('Компонент смонтирован');
}, []);
// Выполняется при изменении count (аналог componentDidUpdate)
useEffect(() => {
console.log('Count изменился:', count);
}, [count]);
// Выполняется при изменении count или name
useEffect(() => {
console.log('Count или name изменились:', count, name);
}, [count, name]);
Очистка эффектов
import React, { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
// Функция очистки (аналог componentWillUnmount)
return () => {
clearInterval(interval);
};
}, []);
return <div>Прошло секунд: {seconds}</div>;
}
Работа с подписками
import React, { useState, useEffect } from 'react';
function ChatComponent() {
const [messages, setMessages] = useState([]);
useEffect(() => {
// Подписка на WebSocket
const socket = new WebSocket('ws://localhost:8080');
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prevMessages => [...prevMessages, message]);
};
// Очистка подписки
return () => {
socket.close();
};
}, []);
return (
<div>
{messages.map((message, index) => (
<div key={index}>{message.text}</div>
))}
</div>
);
}
Множественные useEffect
import React, { useState, useEffect } from 'react';
function UserDashboard({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
// Эффект для загрузки пользователя
useEffect(() => {
const fetchUser = async () => {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
};
fetchUser();
}, [userId]);
// Эффект для загрузки постов
useEffect(() => {
const fetchPosts = async () => {
const response = await fetch(`/api/users/${userId}/posts`);
const postsData = await response.json();
setPosts(postsData);
setLoading(false);
};
fetchPosts();
}, [userId]);
if (loading) return <div>Загрузка...</div>;
return (
<div>
<h1>{user?.name}</h1>
<div>
{posts.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
))}
</div>
</div>
);
}
4. useContext — контекст
Основы Context API
useContext
позволяет потреблять данные из React Context без необходимости передавать пропсы через каждый уровень компонентов:
import React, { createContext, useContext, useState } from 'react';
// Создание контекста
const ThemeContext = createContext();
const UserContext = createContext();
// Провайдер контекста
function App() {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState({ name: 'Иван', role: 'user' });
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<UserContext.Provider value={{ user, setUser }}>
<Header />
<MainContent />
<Footer />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
// Компонент, использующий контекст
function Header() {
const { theme, setTheme } = useContext(ThemeContext);
const { user } = useContext(UserContext);
return (
<header style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}>
<h1>Добро пожаловать, {user.name}!</h1>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Переключить тему
</button>
</header>
);
}
Создание кастомного контекста
import React, { createContext, useContext, useState, useEffect } from 'react';
// Создание контекста для аутентификации
const AuthContext = createContext();
// Кастомный хук для использования аутентификации
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth должен использоваться внутри AuthProvider');
}
return context;
}
// Провайдер аутентификации
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Проверка токена в localStorage
const token = localStorage.getItem('authToken');
if (token) {
// Валидация токена на сервере
validateToken(token);
} else {
setLoading(false);
}
}, []);
const login = async (email, password) => {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('authToken', data.token);
setUser(data.user);
return { success: true };
} else {
return { success: false, error: data.message };
}
} catch (error) {
return { success: false, error: 'Ошибка сети' };
}
};
const logout = () => {
localStorage.removeItem('authToken');
setUser(null);
};
const validateToken = async (token) => {
try {
const response = await fetch('/api/auth/validate', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
} else {
localStorage.removeItem('authToken');
}
} catch (error) {
localStorage.removeItem('authToken');
} finally {
setLoading(false);
}
};
const value = {
user,
login,
logout,
loading
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// Компонент входа
function LoginForm() {
const { login } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
const result = await login(email, password);
if (!result.success) {
alert(result.error);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Пароль"
/>
<button type="submit">Войти</button>
</form>
);
}
// Защищённый компонент
function ProtectedComponent() {
const { user, logout } = useAuth();
return (
<div>
<h2>Добро пожаловать, {user.name}!</h2>
<button onClick={logout}>Выйти</button>
</div>
);
}
Контекст для темы
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme должен использоваться внутри ThemeProvider');
}
return context;
}
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
const themeStyles = {
light: {
backgroundColor: '#ffffff',
color: '#333333',
primaryColor: '#007bff',
secondaryColor: '#6c757d'
},
dark: {
backgroundColor: '#1a1a1a',
color: '#ffffff',
primaryColor: '#4dabf7',
secondaryColor: '#adb5bd'
}
};
const value = {
theme,
toggleTheme,
styles: themeStyles[theme]
};
return (
<ThemeContext.Provider value={value}>
<div style={{
backgroundColor: value.styles.backgroundColor,
color: value.styles.color,
minHeight: '100vh'
}}>
{children}
</div>
</ThemeContext.Provider>
);
}
// Компонент с темой
function ThemedComponent() {
const { theme, toggleTheme, styles } = useTheme();
return (
<div>
<h1 style={{ color: styles.primaryColor }}>
Текущая тема: {theme}
</h1>
<button
onClick={toggleTheme}
style={{
backgroundColor: styles.primaryColor,
color: '#ffffff',
border: 'none',
padding: '10px 20px',
borderRadius: '5px',
cursor: 'pointer'
}}
>
Переключить тему
</button>
</div>
);
}
5. Лучшие практики использования Hooks
Правила Hooks
// ❌ Неправильно — хук в условии
function Component({ condition }) {
if (condition) {
const [state, setState] = useState(0);
}
return <div>...</div>;
}
// ✅ Правильно — хук всегда вызывается
function Component({ condition }) {
const [state, setState] = useState(0);
if (condition) {
// Логика для условия
}
return <div>...</div>;
}
Кастомные Hooks
import { useState, useEffect } from 'react';
// Кастомный хук для загрузки данных
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Кастомный хук для localStorage
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// Кастомный хук для debounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Использование кастомных хуков
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
if (loading) return <div>Загрузка...</div>;
if (error) return <div>Ошибка: {error}</div>;
return (
<div>
<h1>{user.name}</h1>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Поиск..."
/>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Тема: {theme}
</button>
</div>
);
}
Оптимизация производительности
import React, { useState, useCallback, useMemo } from 'react';
function ExpensiveComponent({ items }) {
const [filter, setFilter] = useState('');
// Мемоизация вычислений
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
// Мемоизация функций
const handleItemClick = useCallback((itemId) => {
console.log('Клик по элементу:', itemId);
}, []);
return (
<div>
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Фильтр..."
/>
<ul>
{filteredItems.map(item => (
<li key={item.id} onClick={() => handleItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
</div>
);
}
6. Типизация с TypeScript
import React, { useState, useEffect, useContext, createContext } from 'react';
// Типы для контекста
interface User {
id: number;
name: string;
email: string;
}
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<boolean>;
logout: () => void;
loading: boolean;
}
// Создание типизированного контекста
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// Типизированный кастомный хук
export function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth должен использоваться внутри AuthProvider');
}
return context;
}
// Типизированный кастомный хук для загрузки данных
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result: T = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Неизвестная ошибка');
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Типизированный компонент
interface UserProfileProps {
userId: number;
}
function UserProfile({ userId }: UserProfileProps) {
const { data: user, loading, error } = useFetch<User>(`/api/users/${userId}`);
if (loading) return <div>Загрузка...</div>;
if (error) return <div>Ошибка: {error}</div>;
if (!user) return <div>Пользователь не найден</div>;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
Заключение
React Hooks предоставляют мощный и элегантный способ работы с состоянием и побочными эффектами в функциональных компонентах. useState
позволяет управлять локальным состоянием, useEffect
— выполнять побочные эффекты, а useContext
— потреблять данные из контекста приложения.
Ключевые преимущества Hooks:
- Более простой и читаемый код
- Лучшая композиция логики
- Переиспользование состояния между компонентами
- Более предсказуемое поведение
- Лучшая поддержка TypeScript
При использовании Hooks важно следовать правилам их использования и применять лучшие практики для оптимизации производительности. Кастомные Hooks позволяют инкапсулировать и переиспользовать логику между компонентами, что делает код более модульным и поддерживаемым.