Управление состоянием
Документация по управлению состоянием во фронтенд-приложении.
Введение
Управление состоянием является ключевым аспектом разработки React-приложений. В нашем проекте используется несколько подходов к управлению состоянием в зависимости от требований:
- Локальное состояние компонентов с использованием React Hooks
- Глобальное состояние через Context API
- Состояние форм с использованием React Hook Form
- Управление состоянием серверных данных с использованием TanStack Query
Локальное состояние (useState)
Для управления локальным состоянием компонентов используется хук useState. Это базовый подход для хранения состояния, которое относится только к конкретному компоненту.
// Пример использования useState
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
export default function Counter() {
// Объявление переменной состояния и функции для её обновления
const [count, setCount] = useState(0);
// Функция для увеличения счётчика
const increment = () => setCount(count + 1);
return (
<div className="flex flex-col items-center gap-4">
<p className="text-xl">Значение счётчика: {count}</p>
<Button onClick={increment}>Увеличить</Button>
</div>
);
}
Для управления более сложным состоянием можно объединять несколько переменных состояния в объект:
// Управление формой с использованием useState
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export default function ProfileForm() {
const [formData, setFormData] = useState({
firstName: "",
lastName: "",
email: ""
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log("Form data:", formData);
// Дальнейшая обработка...
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Input
name="firstName"
value={formData.firstName}
onChange={handleChange}
placeholder="Имя"
/>
</div>
<div>
<Input
name="lastName"
value={formData.lastName}
onChange={handleChange}
placeholder="Фамилия"
/>
</div>
<div>
<Input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
</div>
<Button type="submit">Сохранить</Button>
</form>
);
}
Глобальное состояние (Context API)
Для управления глобальным состоянием в приложении используется Context API. Главным примером является AuthContext, который управляет состоянием аутентификации пользователя.
// contexts/AuthContext.tsx
"use client";
import React, { createContext, useContext, useEffect, useState } from "react";
interface User {
id: number;
email: string;
first_name?: string;
last_name?: string;
is_verified: boolean;
oauth_provider?: string;
oauth_id?: string;
profile_image_url?: string;
}
interface AuthContextType {
user: User | null;
isLoading: boolean;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
register: (
email: string,
password: string,
firstName?: string,
lastName?: string
) => Promise<void>;
logout: () => Promise<void>;
refreshUser: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const refreshUser = async () => {
try {
const response = await fetch("/api/auth/me");
if (response.ok) {
const userData = await response.json();
setUser(userData);
} else {
setUser(null);
}
} catch (error) {
console.error("Error fetching user:", error);
setUser(null);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
refreshUser();
}, []);
const login = async (email: string, password: string) => {
// Реализация логики входа
try {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Login failed");
}
await refreshUser();
} catch (error) {
console.error("Login error:", error);
throw error;
}
};
// Другие методы: register, logout, ...
return (
<AuthContext.Provider
value={{
user,
isLoading,
isAuthenticated: !!user,
login,
register,
logout,
refreshUser,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
Для использования AuthContext в компонентах необходимо обернуть приложение в AuthProvider в корневом layout:
// app/layout.tsx
import { AuthProvider } from "@/contexts/AuthContext";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ru">
<body>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
);
}
Доступ к контексту в компонентах осуществляется через хук useAuth:
// Пример использования useAuth
"use client";
import { useAuth } from "@/contexts/AuthContext";
import { Button } from "@/components/ui/button";
export default function ProfileButton() {
const { user, isAuthenticated, isLoading, logout } = useAuth();
if (isLoading) {
return <p>Загрузка...</p>;
}
if (!isAuthenticated) {
return <Button>Войти</Button>;
}
return (
<div className="flex gap-2">
<p>Привет, {user?.first_name || 'Пользователь'}</p>
<Button variant="outline" onClick={logout}>Выйти</Button>
</div>
);
}
Управление состоянием форм
Для эффективного управления состоянием форм в проекте используется React Hook Form в сочетании с Zod для валидации.
// Пример формы с React Hook Form и Zod
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
// Определение схемы валидации с помощью Zod
const formSchema = z.object({
name: z.string().min(2, "Имя должно содержать не менее 2 символов"),
email: z.string().email("Введите корректный email"),
password: z.string().min(8, "Пароль должен содержать не менее 8 символов"),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Пароли не совпадают",
path: ["confirmPassword"],
});
export default function RegisterForm() {
// Инициализация формы с использованием useForm
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
email: "",
password: "",
confirmPassword: "",
},
});
// Обработчик отправки формы
const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
// Отправка данных на сервер
const response = await fetch("/api/auth/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: values.name,
email: values.email,
password: values.password,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
toast.success("Регистрация прошла успешно");
// Дополнительная логика после успешной регистрации
} catch (error) {
toast.error(error instanceof Error ? error.message : "Произошла ошибка");
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Имя</FormLabel>
<FormControl>
<Input placeholder="Введите ваше имя" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="example@email.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Пароль</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Подтверждение пароля</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? "Отправка..." : "Зарегистрироваться"}
</Button>
</form>
</Form>
);
}
Управление состоянием серверных данных
Для эффективного управления состоянием данных, получаемых с сервера, в проекте рекомендуется использовать TanStack Query (React Query). Эта библиотека предоставляет мощные механизмы кэширования, обновления и синхронизации данных.
// lib/queries/users.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import api from "@/lib/api";
import { User, CreateUserInput, UpdateUserInput } from "@/types";
// Ключи запросов для кэширования
export const userKeys = {
all: ["users"] as const,
lists: () => [...userKeys.all, "list"] as const,
list: (filters: Record<string, string>) => [...userKeys.lists(), filters] as const,
details: () => [...userKeys.all, "detail"] as const,
detail: (id: number) => [...userKeys.details(), id] as const,
};
// Хук для получения списка пользователей
export function useUsers(filters: Record<string, string> = {}) {
return useQuery({
queryKey: userKeys.list(filters),
queryFn: () => api.get<User[]>("/users", { params: filters }),
});
}
// Хук для получения одного пользователя
export function useUser(id: number) {
return useQuery({
queryKey: userKeys.detail(id),
queryFn: () => api.get<User>(`/users/${id}`),
enabled: !!id, // Запрос выполняется только если id существует
});
}
// Хук для создания пользователя
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateUserInput) => api.post<User>("/users", data),
onSuccess: () => {
// Инвалидируем кэш списка пользователей после успешного создания
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});
}
// Хук для обновления пользователя
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: UpdateUserInput }) =>
api.patch<User>(`/users/${id}`, data),
onSuccess: (data) => {
// Обновляем кэш после успешного обновления
queryClient.invalidateQueries({ queryKey: userKeys.detail(data.id) });
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});
}
// Хук для удаления пользователя
export function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => api.delete<void>(`/users/${id}`),
onSuccess: (_, id) => {
// Инвалидируем кэш после успешного удаления
queryClient.invalidateQueries({ queryKey: userKeys.detail(id) });
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});
}
Для использования TanStack Query необходимо обернуть приложение в QueryProvider:
// app/providers.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { useState } from "react";
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 минута
refetchOnWindowFocus: false,
},
},
}));
return (
<QueryClientProvider client={queryClient}>
{children}
{process.env.NODE_ENV !== "production" && <ReactQueryDevtools />}
</QueryClientProvider>
);
}
И добавить его в корневой layout:
// app/layout.tsx
import { AuthProvider } from "@/contexts/AuthContext";
import { Providers } from "./providers";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ru">
<body>
<Providers>
<AuthProvider>
{children}
</AuthProvider>
</Providers>
</body>
</html>
);
}
Пример использования TanStack Query в компоненте:
// Пример компонента со списком пользователей
"use client";
import { useUsers } from "@/lib/queries/users";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export default function UsersList() {
const [page, setPage] = useState(1);
const { data, isLoading, error } = useUsers({ page: page.toString() });
if (isLoading) {
return <div>Загрузка...</div>;
}
if (error) {
return <div>Ошибка: {error.message}</div>;
}
return (
<div className="space-y-4">
<h2 className="text-xl font-bold">Список пользователей</h2>
<ul className="space-y-2">
{data?.map(user => (
<li key={user.id} className="p-2 border rounded">
{user.first_name} {user.last_name} ({user.email})
</li>
))}
</ul>
<div className="flex gap-2">
<Button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
>
Предыдущая
</Button>
<Button
onClick={() => setPage(p => p + 1)}
disabled={!data || data.length === 0}
>
Следующая
</Button>
</div>
</div>
);
}
Рекомендации по управлению состоянием
- Используйте подходящий уровень состояния: локальное состояние для компонентов, контекст для общих данных между компонентами, React Query для серверных данных.
- Разделяйте состояние логически: создавайте отдельные контексты для разных доменных областей (auth, user preferences, theme, и т.д.).
- Избегайте избыточного состояния: храните в состоянии только необходимые данные, вычисляемые значения лучше получать через функции.
- Используйте мемоизацию: применяйте useMemo и useCallback для оптимизации производительности при работе со сложными вычислениями или колбэк-функциями.
- Функциональные обновления: используйте форму
setState(prev => newState)
вместоsetState(newState)
для предотвращения проблем с устаревшими значениями состояния. - Сохраняйте иммутабельность: всегда создавайте новые объекты/массивы при обновлении состояния, не изменяйте существующие напрямую.