Основы Redux, Часть 8: Современный Redux с Redux Toolkit
Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →
- Как упростить логику Redux с помощью Redux Toolkit
- Следующие шаги для изучения и использования Redux
Поздравляем, вы добрались до последнего раздела этого руководства! Нам осталось обсудить ещё одну важную тему.
Если хотите освежить в памяти пройденный материал, взгляните на этот краткий обзор:
Recap: What You've Learned
- Part 1: Overview:
- what Redux is, when/why to use it, and the basic pieces of a Redux app
- Part 2: Concepts and Data Flow:
- How Redux uses a "one-way data flow" pattern
- Part 3: State, Actions, and Reducers:
- Redux state is made of plain JS data
- Actions are objects that describe "what happened" events in an app
- Reducers take current state and an action, and calculate a new state
- Reducers must follow rules like "immutable updates" and "no side effects"
- Part 4: Store:
- The
createStoreAPI creates a Redux store with a root reducer function - Stores can be customized using "enhancers" and "middleware"
- The Redux DevTools extension lets you see how your state changes over time
- The
- Part 5: UI and React:
- Redux is separate from any UI, but frequently used with React
- React-Redux provides APIs to let React components talk to Redux stores
useSelectorreads values from Redux state and subscribes to updatesuseDispatchlets components dispatch actions<Provider>wraps your app and lets components access the store
- Part 6: Async Logic and Data Fetching:
- Redux middleware allow writing logic that has side effects
- Middleware add an extra step to the Redux data flow, enabling async logic
- Redux "thunk" functions are the standard way to write basic async logic
- Part 7: Standard Redux Patterns:
- Action creators encapsulate preparing action objects and thunks
- Memoized selectors optimize calculating transformed data
- Request status should be tracked with loading state enum values
- Normalized state makes it easier to look up items by IDs
Как вы убедились, многие аспекты Redux требуют написания многословного кода: иммутабельные обновления, типы действий и их создатели, нормализация состояния. У этих паттернов есть веские основания, но ручное написание такого кода может быть сложным. Кроме того, настройка хранилища Redux включает несколько шагов, и нам приходилось самостоятельно реализовывать логику для диспетчеризации действий загрузки в thunk-функциях или обработки нормализованных данных. Наконец, многие пользователи не уверены в "правильном способе" написания логики Redux.
Поэтому команда Redux создала Redux Toolkit: наш официальный, экспертный инструментарий "всё включено" для эффективной разработки на Redux.
Redux Toolkit содержит пакеты и функции, которые мы считаем необходимыми для создания Redux-приложений. Инструментарий включает рекомендованные лучшие практики, упрощает большинство задач Redux, предотвращает распространённые ошибки и облегчает написание приложений на Redux.
Поэтому Redux Toolkit стал стандартным способом написания логики Redux-приложений. "Ручная" реализация логики Redux, которую вы писали в этом руководстве, является работоспособным кодом, но вам не следует писать логику Redux вручную — мы рассмотрели эти подходы, чтобы вы поняли, как работает Redux. Однако в реальных приложениях вы должны использовать Redux Toolkit для написания логики Redux.
При использовании Redux Toolkit все рассмотренные концепции (действия, редьюсеры, настройка хранилища, создатели действий, санки и т.д.) остаются, но Redux Toolkit предоставляет более простые способы написания этого кода.
Redux Toolkit охватывает только логику Redux — мы по-прежнему используем React-Redux для взаимодействия React-компонентов с хранилищем, включая useSelector и useDispatch.
Давайте посмотрим, как Redux Toolkit упрощает код нашего приложения для управления задачами. Мы перепишем файлы "срезов" (slices), сохранив всю UI-логику без изменений.
Перед продолжением добавьте пакет Redux Toolkit в ваше приложение:
npm install @reduxjs/toolkit
Настройка хранилища
Настройка хранилища Redux проходила несколько итераций. Сейчас код выглядит так:
import { combineReducers } from 'redux'
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'
const rootReducer = combineReducers({
// Define a top-level state field named `todos`, handled by `todosReducer`
todos: todosReducer,
filters: filtersReducer
})
export default rootReducer
import { createStore, applyMiddleware } from 'redux'
import { thunk } from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducer'
const composedEnhancer = composeWithDevTools(applyMiddleware(thunk))
const store = createStore(rootReducer, composedEnhancer)
export default store
Обратите внимание, что процесс настройки включает несколько шагов. Нам необходимо:
-
Объединить редьюсеры срезов в корневой редьюсер
-
Импортировать корневой редьюсер в файл хранилища
-
Импортировать санк-мидлвар, API
applyMiddlewareиcomposeWithDevTools -
Создать усилитель хранилища с мидлваром и инструментами разработчика
-
Создать хранилище с корневым редьюсером
Хорошо бы сократить количество шагов.
Использование configureStore
Redux Toolkit предоставляет API configureStore, упрощающее настройку хранилища. configureStore оборачивает базовый createStore из Redux и автоматизирует большую часть процесса. Фактически мы можем сократить всё до одного шага:
import { configureStore } from '@reduxjs/toolkit'
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'
const store = configureStore({
reducer: {
// Define a top-level state field named `todos`, handled by `todosReducer`
todos: todosReducer,
filters: filtersReducer
}
})
export default store
Один вызов configureStore выполнил всю работу:
-
Объединил
todosReducerиfiltersReducerв корневой редьюсер, обрабатывающий состояние вида{todos, filters} -
Создал хранилище Redux с этим редьюсером
-
Автоматически добавил
thunk-мидлвар -
Добавил дополнительную мидлвару для выявления распространённых ошибок (например, случайной мутации состояния)
-
Настроил подключение Redux DevTools Extension
Убедимся в работе на примере приложения для управления задачами. Весь существующий функционал работает корректно! Поскольку действия диспатчатся, санки выполняются, состояние читается в UI, а история действий отображается в DevTools — значит все компоненты функционируют правильно. Мы лишь изменили код настройки хранилища.
Посмотрим, что произойдёт при случайной мутации состояния. Изменим редьюсер загрузки задач, напрямую изменяя поле состояния вместо иммутабельного обновления:
export default function todosReducer(state = initialState, action) {
switch (action.type) {
// omit other cases
case 'todos/todosLoading': {
// ❌ WARNING: example only - don't do this in a normal reducer!
state.status = 'loading'
return state
}
default:
return state
}
}
Ой! Приложение упало! Что случилось?

Это сообщение об ошибке — хороший знак, мы поймали баг! configureStore добавил специальную мидлвару, которая автоматически выбрасывает ошибку при обнаружении мутации состояния (только в режиме разработки). Это помогает отлавливать ошибки при написании кода.
Оптимизация пакетов
Redux Toolkit уже включает несколько используемых пакетов (redux, redux-thunk, reselect) и реэкспортирует их API. Это позволяет оптимизировать проект.
Сначала изменим импорт createSelector на '@reduxjs/toolkit' вместо 'reselect'. Затем удалим лишние зависимости из package.json:
npm uninstall redux redux-thunk reselect
Важно уточнить: мы по-прежнему используем эти пакеты, и они должны быть установлены. Однако, поскольку Redux Toolkit зависит от них, они будут установлены автоматически при установке @reduxjs/toolkit. Поэтому нам не нужно отдельно указывать эти пакеты в файле package.json.
Написание срезов (Slices)
По мере добавления новых функций в наше приложение файлы срезов становились больше и сложнее. В частности, todosReducer стал сложнее для чтения из-за вложенных операторов spread для иммутабельных обновлений, и нам пришлось писать несколько функций-создателей действий.
Redux Toolkit предоставляет API createSlice, которое поможет упростить нашу логику редьюсеров и действий. createSlice выполняет несколько важных функций:
-
Мы можем писать case-редьюсеры как функции внутри объекта вместо использования конструкции
switch/case -
Редьюсеры смогут использовать более короткую логику иммутабельных обновлений
-
Все создатели действий будут автоматически сгенерированы на основе предоставленных функций-редьюсеров
Использование createSlice
createSlice принимает объект с тремя основными полями:
-
name: строка, которая будет использоваться как префикс для генерируемых типов действий -
initialState: начальное состояние редьюсера -
reducers: объект, где ключи — это строки, а значения — функции-"редьюсеры случаев" (case reducer), которые будут обрабатывать определённые действия
Сначала рассмотрим небольшой самостоятельный пример:
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
entities: [],
status: null
}
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {
// ✅ This "mutating" code is okay inside of createSlice!
state.entities.push(action.payload)
},
todoToggled(state, action) {
const todo = state.entities.find(todo => todo.id === action.payload)
todo.completed = !todo.completed
},
todosLoading(state, action) {
return {
...state,
status: 'loading'
}
}
}
})
export const { todoAdded, todoToggled, todosLoading } = todosSlice.actions
export default todosSlice.reducer
В этом примере следует обратить внимание на несколько моментов:
-
Мы пишем функции case-редьюсеров внутри объекта
reducersи даём им понятные имена -
createSliceавтоматически генерирует создателей действий для каждой функции-редьюсера -
createSliceавтоматически возвращает текущее состояние в случае по умолчанию -
createSliceпозволяет безопасно "мутировать" наше состояние! -
Но при желании мы также можем создавать иммутабельные копии, как раньше
Сгенерированные создатели действий будут доступны как slice.actions.todoAdded. Обычно мы деструктурируем и экспортируем их по отдельности, как это делали с создателями действий, которые писали ранее. Полная функция редьюсера доступна как slice.reducer, и обычно мы делаем export default slice.reducer, снова как и раньше.
Как же выглядят эти автоматически сгенерированные объекты действий? Давайте вызовем один из них и залогируем действие, чтобы увидеть:
console.log(todoToggled(42))
// {type: 'todos/todoToggled', payload: 42}
createSlice сгенерировал строку типа действия, объединив поле name среза с именем функции редьюсера todoToggled. По умолчанию создатель действий принимает один аргумент, который помещается в объект действия как action.payload.
Внутри сгенерированной функции редьюсера createSlice проверяет, соответствует ли action.type диспетчеризованного действия одному из сгенерированных имён. Если соответствует, запускается соответствующий case-редьюсер. Это в точности та же самая модель, которую мы писали вручную с помощью switch/case, но createSlice делает это автоматически.
Также стоит подробнее обсудить аспект "мутации".
Иммутабельные обновления с Immer
Ранее мы обсуждали "мутацию" (изменение существующих объектов/массивов) и "иммутабельность" (трактовку значений как неизменяемых).
В Redux редьюсерам запрещено изменять оригинальные/текущие значения состояния!
// ❌ Illegal - by default, this will mutate the state!
state.value = 123
Если нельзя изменять оригиналы, как тогда возвращать обновлённое состояние?
Редьюсеры могут создавать только копии исходных значений, а затем изменять эти копии.
// This is safe, because we made a copy
return {
...state,
value: 123
}
Как вы видели на протяжении этого руководства, мы можем писать иммутабельные обновления вручную, используя операторы spread JavaScript для массивов/объектов и другие функции, возвращающие копии исходных значений. Однако написание логики иммутабельных обновлений вручную действительно сложно, и случайная мутация состояния в редьюсерах — самая распространённая ошибка пользователей Redux.
Поэтому функция createSlice из Redux Toolkit упрощает написание иммутабельных обновлений!
createSlice использует библиотеку Immer. Immer применяет специальный инструмент JavaScript — Proxy — для обёртки ваших данных, позволяя писать код, который "мутирует" эти обёрнутые данные. Но Immer отслеживает все изменения и возвращает безопасное иммутабельно обновлённое значение, как если бы вы вручную написали всю логику обновления.
Вместо такого кода:
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
Вы можете писать так:
function reducerWithImmer(state, action) {
state.first.second[action.someId].fourth = action.someValue
}
Это гораздо удобнее для чтения!
Но запомните важное правило:
Вы можете писать "мутирующую" логику только в createSlice и createReducer из Redux Toolkit, так как внутри они используют Immer! Если написать мутирующую логику в редьюсерах без Immer, это приведёт к изменению состояния и ошибкам!
Immer всё равно позволяет писать иммутабельные обновления вручную и возвращать новое значение при необходимости. Можно даже смешивать подходы. Например, удаление элемента из массива часто удобнее делать через array.filter(), поэтому можно вызвать этот метод и присвоить результат state, "изменив" его:
// can mix "mutating" and "immutable" code inside of Immer:
state.todos = state.todos.filter(todo => todo.id !== action.payload)
Конвертация редьюсера задач
Начнём конвертацию нашего слайса задач на использование createSlice. Сначала возьмём несколько конкретных кейсов из switch-оператора, чтобы показать процесс.
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
status: 'idle',
entities: {}
}
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {
const todo = action.payload
state.entities[todo.id] = todo
},
todoToggled(state, action) {
const todoId = action.payload
const todo = state.entities[todoId]
todo.completed = !todo.completed
}
}
})
export const { todoAdded, todoToggled } = todosSlice.actions
export default todosSlice.reducer
Редьюсер задач в нашем примере использует нормализованное состояние, вложенное в родительский объект, поэтому код здесь отличается от миниатюрного примера createSlice, который мы рассматривали. Помните, как нам пришлось писать много вложенных операторов spread для переключения задачи? Теперь тот же код стал гораздо короче и читабельнее.
Добавим ещё несколько кейсов в этот редьюсер.
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {
const todo = action.payload
state.entities[todo.id] = todo
},
todoToggled(state, action) {
const todoId = action.payload
const todo = state.entities[todoId]
todo.completed = !todo.completed
},
todoColorSelected: {
reducer(state, action) {
const { color, todoId } = action.payload
state.entities[todoId].color = color
},
prepare(todoId, color) {
return {
payload: { todoId, color }
}
}
},
todoDeleted(state, action) {
delete state.entities[action.payload]
}
}
})
export const { todoAdded, todoToggled, todoColorSelected, todoDeleted } =
todosSlice.actions
export default todosSlice.reducer
Генераторы действий todoAdded и todoToggled принимают только один параметр — объект задачи или её ID. Но что, если нужно передать несколько параметров или выполнить "подготовительную" логику, например, сгенерировать уникальный ID?
createSlice позволяет обрабатывать такие ситуации через "prepare callback". Мы можем передать объект с функциями reducer и prepare. При вызове генератора действия функция prepare получит переданные параметры. Она должна создать и вернуть объект с полем payload (или опционально meta/error), соответствующий конвенции Flux Standard Action.
Здесь мы используем prepare callback, чтобы генератор действия todoColorSelected принимал отдельно todoId и color, объединяя их в объект внутри action.payload.
В редьюсере todoDeleted мы можем использовать оператор delete для удаления элементов из нормализованного состояния.
Эти же паттерны можно применить для конвертации остальных редьюсеров в todosSlice.js и filtersSlice.js.
Вот как выглядит код после конвертации всех слайсов:
Написание санков
Мы уже видели, как писать санки, диспатчащие действия "загрузка", "успех" и "ошибка". Приходилось создавать генераторы действий, типы действий и редьюсеры для их обработки.
Поскольку этот паттерн очень распространён, Redux Toolkit предоставляет API createAsyncThunk для автоматической генерации таких санков. Оно также генерирует типы и генераторы действий для разных статусов запроса и автоматически диспатчит их на основе результата Promise.
Redux Toolkit включает новый API для запросов RTK Query. RTK Query — специализированное решение для запросов и кеширования в Redux-приложениях, которое может полностью избавить от написания санков и редьюсеров для работы с данными. Рекомендуем попробовать его для упрощения кода запросов в ваших приложениях!
Скоро мы обновим учебники Redux, добавив разделы по RTK Query. А пока изучите документацию RTK Query.
Использование createAsyncThunk
Заменим наш санк fetchTodos, сгенерировав его через createAsyncThunk.
createAsyncThunk принимает два аргумента:
-
Строку, которая будет использоваться как префикс для генерируемых типов действий
-
Функция обратного вызова "создатель полезной нагрузки" (payload creator), которая должна возвращать Promise. Обычно её пишут с использованием синтаксиса
async/await, так какasync-функции автоматически возвращают промис.
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
// omit imports and state
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/fakeApi/todos')
return response.todos
})
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// omit reducer cases
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
const newEntities = {}
action.payload.forEach(todo => {
newEntities[todo.id] = todo
})
state.entities = newEntities
state.status = 'idle'
})
}
})
// omit exports
Мы передаём 'todos/fetchTodos' как строковый префикс и функцию "создателя полезной нагрузки", которая вызывает наш API и возвращает промис с полученными данными. Внутри createAsyncThunk сгенерирует три создателя действий (action creators) и типа действий (action types), а также функцию thunk, которая автоматически диспатчит эти действия при вызове. В данном случае создатели действий и их типы:
-
fetchTodos.pending:todos/fetchTodos/pending -
fetchTodos.fulfilled:todos/fetchTodos/fulfilled -
fetchTodos.rejected:todos/fetchTodos/rejected
Однако эти создатели действий и типы определяются вне вызова createSlice. Мы не можем обрабатывать их внутри поля createSlice.reducers, потому что оно тоже генерирует новые типы действий. Нам нужен способ, чтобы вызов createSlice мог отслеживать другие типы действий, определённые в другом месте.
createSlice также принимает опцию extraReducers, где мы можем заставить редьюсер слайса обрабатывать другие типы действий. Это поле должно быть функцией обратного вызова с параметром builder, где мы можем вызвать builder.addCase(actionCreator, caseReducer) для отслеживания дополнительных действий.
Здесь мы вызвали builder.addCase(fetchTodos.pending, caseReducer). Когда это действие диспатчится, мы запустим редьюсер, устанавливающий state.status = 'loading', как и раньше при использовании switch-оператора. Мы можем сделать то же для fetchTodos.fulfilled, обработав полученные от API данные.
В качестве другого примера преобразуем saveNewTodo. Этот thunk принимает text новой задачи как параметр и сохраняет его на сервер. Как нам это обработать?
// omit imports
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/fakeApi/todos')
return response.todos
})
export const saveNewTodo = createAsyncThunk('todos/saveNewTodo', async text => {
const initialTodo = { text }
const response = await client.post('/fakeApi/todos', { todo: initialTodo })
return response.todo
})
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// omit case reducers
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
const newEntities = {}
action.payload.forEach(todo => {
newEntities[todo.id] = todo
})
state.entities = newEntities
state.status = 'idle'
})
.addCase(saveNewTodo.fulfilled, (state, action) => {
const todo = action.payload
state.entities[todo.id] = todo
})
}
})
// omit exports and selectors
Процесс для saveNewTodo аналогичен fetchTodos. Мы вызываем createAsyncThunk, передавая префикс действия и создатель полезной нагрузки. Внутри создателя делаем асинхронный API-вызов и возвращаем результат.
В этом случае при вызове dispatch(saveNewTodo(text)) значение text передаётся в создатель полезной нагрузки как первый аргумент.
Хотя мы не будем подробно рассматривать createAsyncThunk, вот краткие примечания:
-
При диспатче thunk можно передать только один аргумент. Для нескольких значений используйте единый объект
-
Создатель полезной нагрузки получает объект вторым аргументом, содержащий
{getState, dispatch}и другие полезные значения -
Thunk диспатчит действие
pendingперед запуском создателя, затемfulfilledилиrejectedв зависимости от успеха/провала промиса
Нормализация состояния
Ранее мы видели "нормализацию" состояния через хранение элементов в объекте с ключами-идентификаторами. Это позволяет находить элементы по ID без перебора массива. Однако ручное обновление такого состояния было громоздким. Использование "мутабельного" стиля с Immer упрощает код, но повторение логики сохраняется — при загрузке разных типов элементов нам приходилось дублировать редьюсеры.
Redux Toolkit включает API createEntityAdapter с готовыми редьюсерами для типичных операций с нормализованным состоянием. Это добавление, обновление и удаление элементов из слайса. createEntityAdapter также генерирует мемоизированные селекторы для чтения значений из хранилища.
Использование createEntityAdapter
Давайте заменим нашу логику нормализованных сущностей на createEntityAdapter.
Вызов createEntityAdapter возвращает объект "адаптер", содержащий несколько готовых функций-редьюсеров, включая:
-
addOne/addMany: добавление новых элементов в состояние -
upsertOne/upsertMany: добавление новых или обновление существующих элементов -
updateOne/updateMany: обновление существующих элементов частичными значениями -
removeOne/removeMany: удаление элементов по ID -
setAll: полная замена всех существующих элементов
Эти функции можно использовать как редьюсеры для кейсов или как "мутирующие хелперы" внутри createSlice.
Адаптер также включает:
-
getInitialState: возвращает объект вида{ ids: [], entities: {} }для хранения нормализованного состояния элементов с массивом их ID -
getSelectors: генерирует стандартный набор селекторных функций
Рассмотрим использование в нашем срезе задач (todos):
import {
createSlice,
createAsyncThunk,
createEntityAdapter
} from '@reduxjs/toolkit'
// omit some imports
const todosAdapter = createEntityAdapter()
const initialState = todosAdapter.getInitialState({
status: 'idle'
})
// omit thunks
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// omit some reducers
// Use an adapter reducer function to remove a todo by ID
todoDeleted: todosAdapter.removeOne,
completedTodosCleared(state, action) {
const completedIds = Object.values(state.entities)
.filter(todo => todo.completed)
.map(todo => todo.id)
// Use an adapter function as a "mutating" update helper
todosAdapter.removeMany(state, completedIds)
}
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
todosAdapter.setAll(state, action.payload)
state.status = 'idle'
})
// Use another adapter function as a reducer to add a todo
.addCase(saveNewTodo.fulfilled, todosAdapter.addOne)
}
})
// omit selectors
Разные функции адаптера принимают разные значения в action.payload в зависимости от операции. Функции "add" и "upsert" принимают один элемент или массив элементов, "remove" функции — один ID или массив ID, и т.д.
getInitialState позволяет добавлять дополнительные поля состояния. Здесь мы добавили поле status, получив итоговое состояние вида {ids, entities, status}, как ранее.
Мы также можем заменить некоторые селекторы. Функция getSelectors генерирует селекторы типа selectAll (возвращает массив всех элементов) и selectById (возвращает один элемент). Поскольку getSelectors не знает расположения данных в общем состоянии Redux, нужно передать селектор, возвращающий этот срез. Заменим наши селекторы, приведя итоговую версию файла с использованием Redux Toolkit:
import {
createSlice,
createSelector,
createAsyncThunk,
createEntityAdapter
} from '@reduxjs/toolkit'
import { client } from '../../api/client'
import { StatusFilters } from '../filters/filtersSlice'
const todosAdapter = createEntityAdapter()
const initialState = todosAdapter.getInitialState({
status: 'idle'
})
// Thunk functions
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/fakeApi/todos')
return response.todos
})
export const saveNewTodo = createAsyncThunk('todos/saveNewTodo', async text => {
const initialTodo = { text }
const response = await client.post('/fakeApi/todos', { todo: initialTodo })
return response.todo
})
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoToggled(state, action) {
const todoId = action.payload
const todo = state.entities[todoId]
todo.completed = !todo.completed
},
todoColorSelected: {
reducer(state, action) {
const { color, todoId } = action.payload
state.entities[todoId].color = color
},
prepare(todoId, color) {
return {
payload: { todoId, color }
}
}
},
todoDeleted: todosAdapter.removeOne,
allTodosCompleted(state, action) {
Object.values(state.entities).forEach(todo => {
todo.completed = true
})
},
completedTodosCleared(state, action) {
const completedIds = Object.values(state.entities)
.filter(todo => todo.completed)
.map(todo => todo.id)
todosAdapter.removeMany(state, completedIds)
}
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
todosAdapter.setAll(state, action.payload)
state.status = 'idle'
})
.addCase(saveNewTodo.fulfilled, todosAdapter.addOne)
}
})
export const {
allTodosCompleted,
completedTodosCleared,
todoAdded,
todoColorSelected,
todoDeleted,
todoToggled
} = todosSlice.actions
export default todosSlice.reducer
export const { selectAll: selectTodos, selectById: selectTodoById } =
todosAdapter.getSelectors(state => state.todos)
export const selectTodoIds = createSelector(
// First, pass one or more "input selector" functions:
selectTodos,
// Then, an "output selector" that receives all the input results as arguments
// and returns a final result value
todos => todos.map(todo => todo.id)
)
export const selectFilteredTodos = createSelector(
// First input selector: all todos
selectTodos,
// Second input selector: all filter values
state => state.filters,
// Output selector: receives both values
(todos, filters) => {
const { status, colors } = filters
const showAllCompletions = status === StatusFilters.All
if (showAllCompletions && colors.length === 0) {
return todos
}
const completedStatus = status === StatusFilters.Completed
// Return either active or completed todos based on filter
return todos.filter(todo => {
const statusMatches =
showAllCompletions || todo.completed === completedStatus
const colorMatches = colors.length === 0 || colors.includes(todo.color)
return statusMatches && colorMatches
})
}
)
export const selectFilteredTodoIds = createSelector(
// Pass our other memoized selector as an input
selectFilteredTodos,
// And derive data in the output selector
filteredTodos => filteredTodos.map(todo => todo.id)
)
Вызываем todosAdapter.getSelectors, передавая селектор state => state.todos, возвращающий этот срез. Адаптер генерирует селектор selectAll, принимающий всё состояние Redux и преобразующий state.todos.entities и state.todos.ids в полный массив объектов задач. Переименуем selectAll в selectTodos через деструктуризацию, аналогично selectById → selectTodoById.
Остальные селекторы по-прежнему используют selectTodos как входные данные, так как он всегда возвращает массив объектов — независимо от того, хранили ли мы массив целиком в state.todos, вложенным массивом или нормализованным объектом. Использование селекторов позволило сохранить остальной код неизменным, а мемоизированные селекторы оптимизировали производительность интерфейса.
Итоги изученного
Поздравляем! Вы завершили обучение по основам Redux!
Теперь вы понимаете принципы работы Redux и его правильное использование:
-
Управление глобальным состоянием приложения
-
Хранение состояния в виде обычных JS-данных
-
Формирование объектов действий (actions), описывающих события
-
Создание редьюсеров, которые обрабатывают текущее состояние и действия, возвращая новое состояние иммутабельно
-
Чтение состояния Redux в React-компонентах через
useSelector -
Отправка действий из React-компонентов через
useDispatch
Кроме того, вы увидели, как Redux Toolkit упрощает написание логики Redux, и почему Redux Toolkit является стандартным подходом для создания реальных приложений на Redux. Изучив написание кода Redux "вручную", вы теперь понимаете, что делают за вас API Redux Toolkit вроде createSlice, избавляя от необходимости писать этот код самостоятельно.
Дополнительная информация о Redux Toolkit, включая руководства и справочник API:
- Документация Redux Toolkit: https://redux-toolkit.js.org
Давайте в последний раз взглянем на готовое приложение для управления задачами, где весь код переведён на использование Redux Toolkit:
И подведём итоги ключевых моментов, изученных в этом разделе:
- Redux Toolkit (RTK) — стандартный способ написания логики Redux
- RTK включает API, упрощающие большинство задач Redux
- RTK оборачивает ядро Redux и включает другие полезные пакеты
configureStoreнастраивает хранилище Redux с оптимальными параметрами- Автоматически комбинирует редьюсеры срезов в корневой редьюсер
- Автоматически настраивает расширение Redux DevTools и middleware для отладки
createSliceупрощает создание действий и редьюсеров Redux- Автоматически генерирует создатели действий на основе имён срезов/редьюсеров
- Редьюсеры могут "мутировать" состояние внутри
createSliceс помощью Immer
createAsyncThunkгенерирует thunk-функции для асинхронных запросов- Автоматически создаёт thunk + создателей действий
pending/fulfilled/rejected - Вызов thunk запускает ваш payload creator и диспатчит действия
- Действия thunk обрабатываются в
createSlice.extraReducers
- Автоматически создаёт thunk + создателей действий
createEntityAdapterпредоставляет редьюсеры + селекторы для нормализованного состояния- Включает функции редьюсеров для типовых задач: добавление/обновление/удаление элементов
- Генерирует мемоизированные селекторы
selectAllиselectById
Следующие шаги в изучении и использовании Redux
Теперь, когда вы завершили это руководство, мы предлагаем несколько вариантов для дальнейшего изучения Redux.
Данное руководство по основам фокусировалось на низкоуровневых аспектах Redux: ручном написании типов действий и иммутабельных обновлений, работе хранилища Redux и middleware, а также причинах использования таких паттернов, как создатели действий и нормализованное состояние. Кроме того, наш пример с задачами довольно мал и не отражает реалистичного процесса разработки полноценного приложения.
Однако наше руководство "Redux Essentials" специально учит создавать приложения "реального мира". Оно фокусируется на "правильном использовании Redux" через Redux Toolkit и рассматривает более реалистичные паттерны, встречающиеся в крупных приложениях. Оно затрагивает многие темы из этого руководства по основам (например, необходимость иммутабельных обновлений в редьюсерах), но с акцентом на создание реального работающего приложения. Мы настоятельно рекомендуем следующим шагом изучить руководство "Redux Essentials".
При этом концепций, рассмотренных в этом руководстве, достаточно для начала создания собственных приложений на React и Redux. Сейчас идеальное время закрепить знания на практике. Если вы не знаете, какой проект создать, посмотрите список идей для приложений для вдохновения.
Раздел Использование Redux содержит информацию о важных концепциях, например структурировании редьюсеров, а руководство по стилю описывает рекомендуемые паттерны и лучшие практики.
Чтобы узнать больше о том, почему создан Redux, какие проблемы он решает и как его следует использовать, прочтите статьи сопровождающего Redux Марка Эриксона: The Tao of Redux, Part 1: Implementation and Intent и The Tao of Redux, Part 2: Practice and Philosophy.
Если вам нужна помощь по Redux, присоединяйтесь к каналу #redux в Discord-сервере Reactiflux.
Спасибо за прочтение этого руководства, желаем успехов в создании приложений с Redux!