Создание глобального состояния
Настало время объединить поученные знания и создать глобальное состояние, используя оба подхода, которые нам уже известны: useContext и useReducer.
Окей, представьте, что вы строите огромный дом (читай: приложение). У вас есть много комнат (или компонентов), и все они делят один Wi-Fi. Конечно, вы могли бы тащить роутер в каждую комнату (передавать пропсы через дерево компонентов), но почему бы не поставить один мощный роутер (использовать глобальное состояние) и не раздавать интернет всем сразу?
Вот тут-то и вступает в игру сочетание useContext и useReducer. С их помощью мы создадим единое хранилище данных, доступное любому компоненту нашего приложения.
Зачем это нужно?
Во-первых, использование комбинации useContext и useReducer — это эффективный способ управления состоянием на уровне всего приложения. Это похоже на создание центра управления, вместо того чтобы разбрасывать управление данными по всему дереву компонентов.
Во-вторых, это делает наш код более читаемым и структурированным. Вы можете сконцентрировать все логику управления состоянием в одном месте, что особенно полезно в больших проектах.
Интеграция useReducer с useContext
Начнем с создания глобального состояния. Для примера мы построим простое приложение задач (todo list), где каждый пользователь сможет добавлять, удалять и отмечать задачи как выполненные.
1. Создаем редьюсер
Сначала определим редьюсер. Редьюсер — это функция, которая определяет, как изменяется наше состояние в ответ на действия.
// types.ts
export interface Todo {
id: number;
text: string;
completed: boolean;
}
export type State = {
todos: Todo[];
};
export type Action =
| { type: "ADD_TODO"; payload: string }
| { type: "REMOVE_TODO"; payload: number }
| { type: "TOGGLE_TODO"; payload: number };
// reducer.ts
const todoReducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TODO":
const newTodo = {
id: Date.now(), // уникальный id
text: action.payload,
completed: false,
};
return { ...state, todos: [...state.todos, newTodo] };
case "REMOVE_TODO":
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload),
};
case "TOGGLE_TODO":
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
),
};
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
};
export default todoReducer;
2. Создаем контекст
Контекст нужен, чтобы сделать данные нашего редьюсера доступными в любом компоненте. Мы создадим контекст, который будет содержать как состояние, так и функции для диспатча действий.
// context.tsx
import React, { createContext, useReducer, ReactNode } from "react";
import todoReducer, { State, Action } from "./reducer";
const initialState: State = {
todos: [],
};
// Создаем контекст
const TodoContext = createContext<{
state: State;
dispatch: React.Dispatch<Action>;
}>({
state: initialState,
dispatch: () => null, // временный заглушка
});
// Провайдер контекста
const TodoProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(todoReducer, initialState);
return (
<TodoContext.Provider value={{ state, dispatch }}>
{children}
</TodoContext.Provider>
);
};
export { TodoContext, TodoProvider };
3. Подключение провайдера
Теперь подключим TodoProvider к нашему приложению. Это позволит всем компонентам внутри TodoProvider получить доступ к глобальному состоянию.
// App.tsx
import React from "react";
import { TodoProvider } from "./context";
import TodoList from "./TodoList";
function App() {
return (
<TodoProvider>
<TodoList />
</TodoProvider>
);
}
export default App;
Использование глобального состояния
Теперь мы готовы использовать наше глобальное состояние. Для этого подключимся к контексту с помощью хука useContext.
4. Используем контекст в компоненте
Создадим компонент для отображения списка задач и добавления новых.
// TodoList.tsx
import React, { useContext, useState } from "react";
import { TodoContext } from "./context";
const TodoList = () => {
const { state, dispatch } = useContext(TodoContext);
const [newTodo, setNewTodo] = useState("");
const addTodo = () => {
if (newTodo.trim()) {
dispatch({ type: "ADD_TODO", payload: newTodo });
setNewTodo("");
}
};
return (
<div>
<h1>Todo List</h1>
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
/>
<button onClick={addTodo}>Add Task</button>
<ul>
{state.todos.map((todo) => (
<li key={todo.id}>
<span
style={{
textDecoration: todo.completed ? "line-through" : "none",
}}
onClick={() => dispatch({ type: "TOGGLE_TODO", payload: todo.id })}
>
{todo.text}
</span>
<button onClick={() => dispatch({ type: "REMOVE_TODO", payload: todo.id })}>
Delete
</button>
</li>
))}
</ul>
</div>
);
};
export default TodoList;
5. Вот и готово ваше Todo-приложение!
Теперь вы можете добавлять, удалять и отмечать задачи как выполненные. А самое важное — наше состояние централизовано и доступно через контекст.
Плюсы и минусы такого подхода
Плюсы глобального состояния с использованием useContext и useReducer:
- Простота: все состояния и логика управления находятся в одном месте.
- Масштабируемость: легко добавлять новые действия и типы данных.
- Типизация: TypeScript делает наш код надежным и предсказуемым.
Однако не забывайте о следующих потенциальных проблемах:
- Если приложение становится большим, контекст может перестать быть оптимальным по производительности — в таком случае стоит рассмотреть Redux или другие библиотеки.
- Частые ререндеры: при изменении состояния весь контекст вызывает обновление, даже если некоторые компоненты не зависят от новых данных.
Теперь вы готовы перенести управление состоянием вашего приложения на новый уровень. useContext и useReducer — потрясающее комбо для небольших и средних приложений. Попрактикуйтесь, добавляя новые фичи в наше Todo-приложение!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ