Redux — одна из самых популярных библиотек для управления состоянием в приложениях. Она помогает структурировать код, делает состояние предсказуемым и значительно упрощает отладку. В этом руководстве мы разберём Redux подробно, начиная с основ и переходя к практике.
В первой части мы узнаем, зачем нужен Redux, разберём его основные принципы и изучим ключевые элементы.
Часть 1: Основы Redux
Что такое Redux и зачем он нужен?
Redux — это библиотека для управления состоянием приложения. Она была создана для упрощения работы с состоянием в сложных приложениях, где данные должны быть доступны разным частям дерева компонентов.
Почему именно Redux?
- Единый источник правды: Всё состояние приложения хранится в одном месте — в объекте, который называется store. Это позволяет легко контролировать, какие данные где используются.
- Предсказуемость: Изменения состояния происходят через чётко определённые действия (actions) и чистые функции (reducers). Это делает логику приложения прозрачной.
- Инструменты отладки: Redux DevTools — мощный инструмент, который позволяет видеть историю изменений состояния, откатываться к предыдущим состояниям и анализировать экшены.
Когда использовать Redux?
Redux нужен не всегда. Вот основные сценарии:
- Приложения со сложным состоянием. Например, если у вас есть аутентификация, данные пользователя, корзина покупок и фильтры, которые должны взаимодействовать.
- Глобальное состояние. Когда данные нужны нескольким компонентам на разных уровнях дерева (например, текущая тема или авторизация).
- Сложная логика обновления. Если состояние обновляется разными компонентами или взаимодействие данных между компонентами сложное.
Когда Redux не нужен?
- Если приложение небольшое, и данные можно передать через props или useState. Например, список задач или счётчик.
- Когда контекст React (useContext) решает задачу глобального состояния.
Основные принципы Redux
Redux основывается на трёх ключевых принципах:
Единый источник правды
Всё состояние приложения хранится в одном объекте store. Это делает управление данными централизованным и удобным.
const initialState = {
user: { name: 'Иван', isLoggedIn: true },
cart: [{ id: 1, name: 'Товар 1', quantity: 2 }],
theme: 'dark',
};
Состояние только для чтения
Состояние нельзя изменить напрямую. Чтобы изменить данные, нужно отправить action — объект, описывающий, что именно должно измениться.
const action = {
type: 'ADD_TO_CART',
payload: { id: 2, name: 'Товар 2', quantity: 1 },
};
Изменения через чистые функции
Логика обновления состояния определяется чистыми функциями, называемыми reducers. Они принимают текущее состояние и экшен, а возвращают новое состояние.
function cartReducer(state = [], action) {
switch (action.type) {
case 'ADD_TO_CART':
return [...state, action.payload];
default:
return state;
}
}
Основные элементы Redux
Чтобы лучше понять, как работает Redux, давайте разберём его основные элементы:
Store
Store — это объект, который хранит состояние вашего приложения. Он создаётся с помощью функции createStore (в Redux Toolkit используется configureStore).
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
Actions
Actions — это объекты, описывающие, что именно должно произойти. Каждый экшен содержит поле type (тип действия) и, при необходимости, дополнительные данные (payload).
const addToCart = {
type: 'ADD_TO_CART',
payload: { id: 2, name: 'Товар 2', quantity: 1 },
};
Reducers
Reducers — это функции, которые определяют, как изменяется состояние. Они принимают два параметра: текущее состояние и экшен.
function cartReducer(state = [], action) {
switch (action.type) {
case 'ADD_TO_CART':
return [...state, action.payload];
default:
return state;
}
}
Dispatch
Чтобы изменить состояние, мы используем метод dispatch. Он отправляет экшен в store, который передаёт его редьюсеру.
store.dispatch({
type: 'ADD_TO_CART',
payload: { id: 3, name: 'Товар 3', quantity: 1 },
});
Selectors
Selectors — это функции, которые извлекают данные из store. Они позволяют уменьшить дублирование кода и улучшить читаемость.
const getCartItems = (state) => state.cart;
const items = getCartItems(store.getState());
Часть 2: Практика с Redux Toolkit
В этой части создадим приложение для управления списком покупок. Это приложение позволит добавлять, удалять и просматривать товары в списке.
Redux Toolkit включает в себя готовые инструменты, которые упрощают создание store и работы с редьюсерами. Мы будем использовать:
- createSlice — для определения состояния, экшенов и редьюсеров.
- configureStore — для настройки и создания store.
- useDispatch и useSelector — хуки для взаимодействия с Redux.
Шаг 1: Установка и настройка Redux Toolkit
Убедитесь, что Redux Toolkit и React Redux установлены:
npm install @reduxjs/toolkit react-redux
Создадим структуру проекта:
src/
store/
index.js // Store
shoppingSlice.js // Slice для списка покупок
components/
ShoppingList.jsx // Компонент списка
AddItemForm.jsx // Форма добавления
App.jsx // Главный компонент
Шаг 2: Настройка store
В src/store/index.js создаём store:
import { configureStore } from '@reduxjs/toolkit';
import shoppingReducer from './shoppingSlice';
const store = configureStore({
reducer: {
shopping: shoppingReducer,
},
});
export default store;
Здесь:
- Мы используем configureStore, чтобы создать store.
- Добавляем редьюсер для управления списком покупок.
Шаг 3: Создание shoppingSlice
В src/store/shoppingSlice.js создаём slice:
import { createSlice } from '@reduxjs/toolkit';
const shoppingSlice = createSlice({
name: 'shopping',
initialState: [],
reducers: {
addItem: (state, action) => {
state.push(action.payload);
},
removeItem: (state, action) => {
return state.filter((_, index) => index !== action.payload);
},
},
});
export const { addItem, removeItem } = shoppingSlice.actions;
export default shoppingSlice.reducer;
Здесь:
- createSlice автоматически создаёт редьюсер и экшены.
- initialState — начальное состояние (пустой массив для списка покупок).
- addItem — добавляет элемент в массив.
- removeItem — удаляет элемент по индексу.
Шаг 4: Подключение Redux к React
В src/index.js оборачиваем приложение в Provider:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Шаг 5: Компоненты приложения
Форма добавления товара
В src/components/AddItemForm.jsx создаём форму для добавления товаров:
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addItem } from '../store/shoppingSlice';
function AddItemForm() {
const [text, setText] = useState('');
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
dispatch(addItem(text)); // Отправляем экшен для добавления
setText(''); // Очищаем поле ввода
}
};
return (
<form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Добавить товар"
style={{ padding: '10px', marginRight: '10px' }}
/>
<button type="submit" style={{ padding: '10px' }}>
Добавить
</button>
</form>
);
}
export default AddItemForm;
Список покупок
В src/components/ShoppingList.jsx создаём список товаров:
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { removeItem } from '../store/shoppingSlice';
function ShoppingList() {
const items = useSelector((state) => state.shopping);
const dispatch = useDispatch();
return (
<ul style={{ listStyle: 'none', padding: 0 }}>
{items.map((item, index) => (
<li
key={index}
style={{
marginBottom: '10px',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '5px',
}}
>
{item}{' '}
<button
onClick={() => dispatch(removeItem(index))}
style={{
marginLeft: '10px',
padding: '5px',
backgroundColor: 'red',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
}}
>
Удалить
</button>
</li>
))}
</ul>
);
}
export default ShoppingList;
Главный компонент
В src/App.jsx объединяем всё:
import React from 'react';
import AddItemForm from './components/AddItemForm';
import ShoppingList from './components/ShoppingList';
function App() {
return (
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
<h1>Список покупок</h1>
<AddItemForm />
<ShoppingList />
</div>
);
}
export default App;
Шаг 6: Запуск приложения
Запустите проект:
npm start
В приложении вы сможете добавлять товары в список и удалять их по одному.
Часть 3: Улучшение приложения
Теперь улучшим наше приложение, добавив дополнительные функции: сохранение данных в localStorage, валидацию и динамическую стилизацию.
Сохранение данных в localStorage
Сейчас данные списка покупок теряются при перезагрузке страницы. Мы можем исправить это, сохраняя данные в localStorage и восстанавливая их при загрузке приложения.
Обновим store/index.js, чтобы загружать данные из localStorage при инициализации:
const loadState = () => {
const savedState = localStorage.getItem('shoppingList');
return savedState ? JSON.parse(savedState) : [];
};
const store = configureStore({
reducer: {
shopping: shoppingSlice.reducer,
},
preloadedState: {
shopping: loadState(),
},
});
Подписываемся на изменения состояния, чтобы сохранять список в localStorage:
store.subscribe(() => {
const state = store.getState();
localStorage.setItem('shoppingList', JSON.stringify(state.shopping));
});
Теперь, даже если вы перезагрузите страницу, список товаров будет восстановлен из localStorage.
Добавление валидации
Сейчас мы можем добавлять пустые товары или дублировать элементы. Исправим это, добавив проверку на уникальность и непустое значение.
Изменим логику добавления товара, чтобы проверять, не пустое ли поле и нет ли уже такого товара в списке:
function AddItemForm() {
const [text, setText] = useState('');
const [error, setError] = useState('');
const dispatch = useDispatch();
const items = useSelector((state) => state.shopping);
const handleSubmit = (e) => {
e.preventDefault();
// Проверяем, что поле не пустое
if (!text.trim()) {
setError('Введите название товара.');
return;
}
// Проверяем, что товар уникален
if (items.includes(text)) {
setError('Товар уже есть в списке.');
return;
}
dispatch(addItem(text));
setText('');
setError('');
};
return (
<form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Добавить товар"
style={{ padding: '10px', marginRight: '10px' }}
/>
<button type="submit" style={{ padding: '10px' }}>
Добавить
</button>
{error && <p style={{ color: 'red', marginTop: '10px' }}>{error}</p>}
</form>
);
}
Теперь:
- Пользователь не сможет добавить пустой товар.
- При попытке добавить дублирующийся товар появится сообщение об ошибке.
Динамическая стилизация
Добавим возможность помечать товары как купленные. Для этого:
- Добавим в каждый товар свойство isPurchased.
- Стилизуем купленные товары.
Обновление shoppingSlice
Добавим новый экшен для переключения статуса «Куплено»:
const shoppingSlice = createSlice({
name: 'shopping',
initialState: [],
reducers: {
addItem: (state, action) => {
state.push({ name: action.payload, isPurchased: false });
},
removeItem: (state, action) => {
return state.filter((_, index) => index !== action.payload);
},
togglePurchased: (state, action) => {
const item = state[action.payload];
if (item) {
item.isPurchased = !item.isPurchased;
}
},
},
});
export const { addItem, removeItem, togglePurchased } = shoppingSlice.actions;
export default shoppingSlice.reducer;
Обновление ShoppingList
Теперь мы можем отображать купленные товары с изменённым стилем:
function ShoppingList() {
const items = useSelector((state) => state.shopping);
const dispatch = useDispatch();
return (
<ul style={{ listStyle: 'none', padding: 0 }}>
{items.map((item, index) => (
<li
key={index}
style={{
marginBottom: '10px',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '5px',
backgroundColor: item.isPurchased ? '#d4edda' : '#f8d7da',
}}
>
<span
style={{
textDecoration: item.isPurchased ? 'line-through' : 'none',
cursor: 'pointer',
}}
onClick={() => dispatch(togglePurchased(index))}
>
{item.name}
</span>
<button
onClick={() => dispatch(removeItem(index))}
style={{
marginLeft: '10px',
padding: '5px',
backgroundColor: 'red',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
}}
>
Удалить
</button>
</li>
))}
</ul>
);
}
Теперь:
- Пользователь может кликнуть по названию товара, чтобы пометить его как купленный.
- Купленные товары будут выделены зелёным фоном и зачёркнуты.
Итоговый код
store/index.js
import { configureStore } from '@reduxjs/toolkit';
import shoppingReducer from './shoppingSlice';
const loadState = () => {
const savedState = localStorage.getItem('shoppingList');
return savedState ? JSON.parse(savedState) : [];
};
const store = configureStore({
reducer: {
shopping: shoppingReducer,
},
preloadedState: {
shopping: loadState(),
},
});
store.subscribe(() => {
const state = store.getState();
localStorage.setItem('shoppingList', JSON.stringify(state.shopping));
});
export default store;
shoppingSlice.js
import { createSlice } from '@reduxjs/toolkit';
const shoppingSlice = createSlice({
name: 'shopping',
initialState: [],
reducers: {
addItem: (state, action) => {
state.push({ name: action.payload, isPurchased: false });
},
removeItem: (state, action) => {
return state.filter((_, index) => index !== action.payload);
},
togglePurchased: (state, action) => {
const item = state[action.payload];
if (item) {
item.isPurchased = !item.isPurchased;
}
},
},
});
export const { addItem, removeItem, togglePurchased } = shoppingSlice.actions;
export default shoppingSlice.reducer;
Комментарии
0