Привет, Хабр!
Сегодня рассмотрим библиотеку Redux для JS, зачем она нужна, и стоит ли она вашего внимания. Redux — это библиотека для управления состоянием приложения. Redux создан для тех случаев, когда:
-
У вас огромное приложение, и нужно управлять кучей данных.
-
Эти данные нужно шарить между компонентами, которые находятся на разных уровнях иерархии.
-
Есть сложная логика обновления данных, и хочется, чтобы код этой логики был не просто работающим, но и понятным через полгода.
Redux помогает:
-
Упорядочить данные.
-
Упростить их доступ из любой точки приложения.
-
Стандартизировать логику изменения данных.
Главный принцип Redux — один источник правды. Все данные приложения хранятся в одном месте — в store
. Если хочется что‑то изменить:
-
Вы диспатчите action (описание того, что должно произойти).
-
Данные обновляются через чистую функцию reducer (чистую — значит без побочных эффектов).
-
Новое состояние становится доступным для всех компонентов.
Чаще всего Redux используется в связке с React, и это неудивительно — react-redux
делает их совместную работу невероятно удобной. Но при этом, Redux вполне может работать с другими фреймворками (или даже без них).
Основной функционал Redux
Для начала установим Redux и его дружка — react-redux
:
npm install redux react-redux
redux
— это ядро библиотеки. А react-redux
— это набор инструментов для интеграции Redux с React.
Создание Store
Начнем с главного — store
. Это центральное хранилище состояния. Все, что вы будете хранить, находится здесь:
import { createStore } from 'redux'; // Начальное состояние const initialState = { counter: 0, }; // Редьюсер — чистая функция, которая обновляет состояние function counterReducer(state = initialState, action) { switch (action.type) { case 'INCREMENT': return { ...state, counter: state.counter + 1 }; case 'DECREMENT': return { ...state, counter: state.counter - 1 }; default: return state; } } // Создаём store const store = createStore(counterReducer); console.log(store.getState()); // { counter: 0 }
Редьюсер получает текущее состояние и действие (action
) и возвращает новое состояние.
Actions: говорим, что делать
Action
— это просто объект с обязательным полем type
. Пример:
const incrementAction = { type: 'INCREMENT' }; const decrementAction = { type: 'DECREMENT' };
Каждое действие говорит редьюсеру: «Давай что‑то сделаем».
Reducer
Вот редьюсер из нашего примера:
function counterReducer(state = initialState, action) { switch (action.type) { case 'INCREMENT': return { ...state, counter: state.counter + 1 }; case 'DECREMENT': return { ...state, counter: state.counter - 1 }; default: return state; } }
-
state
— текущее состояние. -
action
— что мы хотим сделать. -
Редьюсер обязан вернуть новое состояние. Если действие не распознано, возвращаем старое.
Подключение React и Redux
Настало время объединить Redux с React. Это проще, чем кажется, благодаря react-redux
:
Provider
делает store
доступным для всех компонентов:
import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; import counterReducer from './reducers'; const store = createStore(counterReducer); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );
Теперь подключаем хуки useSelector и useDispatch и любой компонент сможет получать данные из store
через useSelector
и отправлять действия через useDispatch
:
import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; function Counter() { const counter = useSelector((state) => state.counter); const dispatch = useDispatch(); return ( <div> <h1>Counter: {counter}</h1> <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button> <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button> </div> ); } export default Counter;
Redux Toolkit
Ванильный Redux требует много шаблонного кода: отдельные экшены, редьюсеры, константы… Все это звучит немного утомительно. Вот почему появился Redux Toolkit. Это набор инструментов, который значительно упрощает работу и именно им все пользуются при работе с Redux на сегодняшний день.
С Redux Toolkit можно:
-
Создавать редьюсеры и экшены в одной функции
createSlice
. -
Избавиться от ручного управления состоянием через Immer.js, встроенный в Toolkit.
-
Упрощать настройки
store
черезconfigureStore
.
Пример:
import { configureStore, createSlice } from '@reduxjs/toolkit'; // Создаем slice (редьюсер + экшены) const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, incrementByAmount: (state, action) => { state.value += action.payload; }, }, }); // Экшены export const { increment, decrement, incrementByAmount } = counterSlice.actions; // Store const store = configureStore({ reducer: { counter: counterSlice.reducer, }, }); export default store;
Теперь вместо того, чтобы писать тонны кода для экшенов и редьюсеров, все это создается автоматом.
Подробнее про Toolkit можно глянуть здесь.
Middleware
Одно из самых мощных, но недооцененных на мой взгляд свойств Redux — это middleware. По сути, это функции, которые сидят между экшенами и редьюсерами, и могут перехватывать действия, добавлять дополнительную логику или даже модифицировать экшены во время их действия.
В Redux middleware
применяются через функцию applyMiddleware
:
import { createStore, applyMiddleware } from 'redux'; import logger from 'redux-logger'; const store = createStore(rootReducer, applyMiddleware(logger));
redux-logger
выводит информацию о каждом экшене и состоянии в консоль.
Асинхронность и Thunk
Redux по дефолту синхронный, но для работы с асинхронными операциями — например, запросами к API — есть специальное middleware redux-thunk
:
npm install redux-thunk
Подключаем redux-thunk
:
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; const store = createStore(rootReducer, applyMiddleware(thunk));
А теперь создадим асинхронный экшен:
// actions/userActions.js export const fetchUsers = () => async (dispatch) => { dispatch({ type: 'FETCH_USERS_REQUEST' }); try { const response = await fetch('https://jsonplaceholder.typicode.com/users'); const data = await response.json(); dispatch({ type: 'FETCH_USERS_SUCCESS', payload: data }); } catch (error) { dispatch({ type: 'FETCH_USERS_FAILURE', payload: error.message }); } };
Теперь Redux может обрабатывать сложные асинхронные сценарии.
Теперь перейдем к практике.
Пример применения библиотеки
Представим, что есть интернет‑магазин, специализирующийся на товарах для котиков: игрушки, корм, лежанки, лазилки и все, что может сделать пушистого клиента счастливым. Нам нужно управлять следующими состояниями приложения:
-
Список товаров: загрузка из API и отображение на странице.
-
Корзина: добавление и удаление товаров.
-
Авторизация пользователя: чтобы позволить зарегистрированным пользователям оформлять заказы.
-
Оформление заказа: управление данными для оплаты и доставки.
Чтобы сделать приложение масштабируемым и поддерживаемым, разобьем состояние на несколько модулей: products, cart, auth, order. Каждый модуль будет представлять свою «часть состояния» (state slice) и работать через Redux Toolkit
.
Подготовка состояния товаров
Сначала настроим модуль для работы с товарами. Допустим, мы загружаем список товаров с сервера.
Slice для товаров:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; // Асинхронный thunk для загрузки товаров export const fetchProducts = createAsyncThunk( 'products/fetchProducts', async () => { const response = await fetch('https://api.example.com/cat-products'); const data = await response.json(); return data; } ); const productsSlice = createSlice({ name: 'products', initialState: { items: [], status: 'idle', // idle | loading | succeeded | failed error: null, }, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchProducts.pending, (state) => { state.status = 'loading'; }) .addCase(fetchProducts.fulfilled, (state, action) => { state.status = 'succeeded'; state.items = action.payload; }) .addCase(fetchProducts.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; }); }, }); export default productsSlice.reducer;
Теперь есть асинхронный экшен fetchProducts
, который загружает товары и обновляет состояние.
Управление корзиной
В корзине нужно уметь добавлять товары, изменять их количество и удалять. Также будем хранить общее количество товаров и сумму заказа.
Slice для корзины:
import { createSlice } from '@reduxjs/toolkit'; const cartSlice = createSlice({ name: 'cart', initialState: { items: [], // [{ id, name, price, quantity }] totalItems: 0, totalPrice: 0, }, reducers: { addToCart: (state, action) => { const product = action.payload; const existingItem = state.items.find((item) => item.id === product.id); if (existingItem) { existingItem.quantity += 1; } else { state.items.push({ ...product, quantity: 1 }); } state.totalItems += 1; state.totalPrice += product.price; }, removeFromCart: (state, action) => { const productId = action.payload; const existingItem = state.items.find((item) => item.id === productId); if (existingItem) { state.totalItems -= existingItem.quantity; state.totalPrice -= existingItem.price * existingItem.quantity; state.items = state.items.filter((item) => item.id !== productId); } }, updateQuantity: (state, action) => { const { id, quantity } = action.payload; const existingItem = state.items.find((item) => item.id === id); if (existingItem) { const quantityDiff = quantity - existingItem.quantity; existingItem.quantity = quantity; state.totalItems += quantityDiff; state.totalPrice += quantityDiff * existingItem.price; } }, }, }); export const { addToCart, removeFromCart, updateQuantity } = cartSlice.actions; export default cartSlice.reducer;
Авторизация пользователя
Авторизация нужна для оформления заказа и сохранения истории покупок. Будем хранить информацию о текущем пользователе в модуле auth
.
Slice для авторизации:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; export const login = createAsyncThunk( 'auth/login', async (credentials) => { const response = await fetch('https://api.example.com/login', { method: 'POST', body: JSON.stringify(credentials), headers: { 'Content-Type': 'application/json' }, }); const data = await response.json(); return data; // { userId, token } } ); const authSlice = createSlice({ name: 'auth', initialState: { user: null, token: null, status: 'idle', error: null, }, reducers: { logout: (state) => { state.user = null; state.token = null; }, }, extraReducers: (builder) => { builder .addCase(login.pending, (state) => { state.status = 'loading'; }) .addCase(login.fulfilled, (state, action) => { state.status = 'succeeded'; state.user = action.payload.userId; state.token = action.payload.token; }) .addCase(login.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; }); }, }); export const { logout } = authSlice.actions; export default authSlice.reducer;
Оформление заказа
После выбора товаров и авторизации отправляем данные заказа на сервер.
Slice для заказа:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; export const submitOrder = createAsyncThunk( 'order/submitOrder', async (orderDetails, { getState }) => { const { token } = getState().auth; const response = await fetch('https://api.example.com/orders', { method: 'POST', body: JSON.stringify(orderDetails), headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, }); return await response.json(); } ); const orderSlice = createSlice({ name: 'order', initialState: { status: 'idle', error: null }, reducers: {}, extraReducers: (builder) => { builder .addCase(submitOrder.pending, (state) => { state.status = 'loading'; }) .addCase(submitOrder.fulfilled, (state) => { state.status = 'succeeded'; }) .addCase(submitOrder.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; }); }, }); export default orderSlice.reducer;
И вот так, шаг за шагом, мы построили интернет‑магазин для котиков, который может справиться с любым капризом пушистых клиентов: от корзины до оформления заказа. Redux стал связующим звеном между всеми частями нашего приложения — управление товарами, авторизация, обработка заказов — все четко и предсказуемо. А благодаря Redux Toolkit обошлись без тонны шаблонного кода.
Что в итоге
Redux — штука мощная, но не панацея. Он идеален, если у вас сложное приложение с кучей состояний, которыми нужно управлять централизованно. Дальше дело за вами: экспериментируйте, пробуйте разные подходы, но не забывайте, что Redux нужен не всегда. Иногда и Context API
за глаза хватит.
Если хочется копнуть глубже:
-
FAQ Redux: https://redux.js.org/faq
-
Redux Toolkit: https://redux‑toolkit.js.org/
-
React‑Redux: https://react‑redux.js.org/
Что ещё изучить? Разберитесь с redux-thunk
и redux-saga
для асинхронщины, гляньте Reselect
для оптимизации селекторов. И обязательно поиграйтесь с Redux DevTools.
Делитесь своим опытом работы с Redux и советами для начинающих!
Также хочется напомнить про открытые уроки, которые пройдут в рамках набора на курс Otus «JavaScript Developer. Professional»:
4 декабря: Создание веб-компонентов и использование Shadow DOM. Узнать подробнее
18 декабря: Управление состоянием с Pinia для Vue3. Узнать подробнее
ссылка на оригинал статьи https://habr.com/ru/articles/863002/
Добавить комментарий