Redux Fundamentals, Часть 6: Асинхронная логика и получение данных
Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →
- Как работает поток данных Redux с асинхронными данными
- Как использовать middleware Redux для асинхронной логики
- Шаблоны обработки состояния асинхронных запросов
- Умение работать с HTTP-запросами для получения и обновления данных с сервера
- Понимание асинхронной логики в JS, включая промисы (Promises)
Введение
В Части 5: UI и React мы рассмотрели, как использовать библиотеку React-Redux для взаимодействия React-компонентов с хранилищем Redux, включая вызов useSelector для чтения состояния Redux, вызов useDispatch для доступа к функции dispatch и обёртывание приложения в компонент <Provider> для предоставления хукам доступа к хранилищу.
До сих пор все данные, с которыми мы работали, находились непосредственно в нашем клиентском приложении React+Redux. Однако большинству реальных приложений необходимо работать с данными с сервера, выполняя HTTP-вызовы API для получения и сохранения элементов.
В этом разделе мы обновим наше приложение для работы с задачами (todo), чтобы оно получало задачи из API и добавляло новые задачи, сохраняя их через API.
Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →
Обратите внимание, что в этом руководстве намеренно используются устаревшие шаблоны логики Redux, требующие больше кода, чем "современные Redux" шаблоны с Redux Toolkit, которые мы рекомендуем как правильный подход для создания приложений на Redux сегодня. Это сделано для объяснения принципов и концепций Redux. Данный материал не предназначен для использования в production-проектах.
Чтобы изучить "современный Redux" с Redux Toolkit, перейдите по ссылкам:
- Полное руководство "Redux Essentials" — научит "правильному использованию Redux" с Redux Toolkit для реальных приложений. Всем изучающим Redux мы настоятельно рекомендуем это руководство!
- Redux Fundamentals, Часть 8: Современный Redux с Redux Toolkit — демонстрирует преобразование низкоуровневых примеров из предыдущих разделов в эквиваленты на Redux Toolkit
Redux Toolkit включает API для получения данных и кэширования RTK Query. RTK Query — это специализированное решение для получения данных и кэширования в приложениях Redux, которое позволяет полностью избежать написания санков (thunks) или редюсеров для управления получением данных. Мы специально преподаём RTK Query как основной подход к получению данных, и он построен на тех же шаблонах, что показаны на этой странице.
Узнайте, как использовать RTK Query для получения данных, в разделе Redux Essentials, Часть 7: Основы RTK Query.
Пример REST API и клиента
Чтобы пример проекта был изолированным, но реалистичным, начальная настройка проекта уже включает фейковое in-memory REST API для наших данных (настроенное с помощью инструмента Mirage.js для мокирования API). API использует /fakeApi как базовый URL для эндпоинтов и поддерживает стандартные HTTP-методы GET/POST/PUT/DELETE для /fakeApi/todos. Оно определено в src/api/server.js.
Проект также содержит небольшой объект HTTP API-клиента, предоставляющий методы client.get() и client.post(), аналогично популярным HTTP-библиотекам вроде axios. Он определён в src/api/client.js.
В этом разделе мы будем использовать объект client для выполнения HTTP-вызовов к нашему фейковому in-memory REST API.
Middleware Redux и побочные эффекты
Само по себе хранилище Redux ничего не знает об асинхронной логике. Оно умеет только синхронно диспетчеризовать действия, обновлять состояние путём вызова корневого редюсера и уведомлять UI об изменениях. Любая асинхронность должна происходить вне хранилища.
Ранее мы говорили, что редюсеры Redux никогда не должны содержать "побочных эффектов". "Побочный эффект" — это любое изменение состояния или поведения, которое можно наблюдать вне возвращаемого функцией значения. Распространённые примеры побочных эффектов:
-
Вывод значения в консоль
-
Сохранение файла
-
Установка асинхронного таймера
-
Выполнение HTTP-запроса
-
Изменение состояния вне функции или мутация аргументов функции
-
Генерация случайных чисел или уникальных ID (например,
Math.random()илиDate.now())
Однако любому реальному приложению где-то нужно выполнять подобные операции. И если мы не можем размещать побочные эффекты в редюсерах, где можно их размещать?
Middleware Redux были разработаны для реализации логики с побочными эффектами.
Как мы говорили в Части 4, Redux middleware может выполнять любые действия при получении диспетчеризованного экшена: логировать данные, модифицировать экшен, задерживать его выполнение, делать асинхронные вызовы и многое другое. Кроме того, поскольку middleware формируют конвейер вокруг реальной функции store.dispatch, это также означает, что мы можем передавать в dispatch нечто, не являющееся обычным объектом экшена — при условии, что middleware перехватит это значение и не допустит его попадания в редюсеры.
Middleware также имеют доступ к dispatch и getState. Это означает, что вы можете написать асинхронную логику внутри middleware и сохранить возможность взаимодействия с Redux-хранилищем путём диспетчеризации экшенов.
Использование Middleware для асинхронной логики
Рассмотрим несколько примеров того, как middleware позволяет реализовать асинхронную логику, взаимодействующую с Redux-хранилищем.
Один из вариантов — написание middleware, который ищет определённые типы экшенов и выполняет асинхронную логику при их обнаружении, как в этих примерах:
import { client } from '../api/client'
const delayedActionMiddleware = storeAPI => next => action => {
if (action.type === 'todos/todoAdded') {
setTimeout(() => {
// Delay this action by one second
next(action)
}, 1000)
return
}
return next(action)
}
const fetchTodosMiddleware = storeAPI => next => action => {
if (action.type === 'todos/fetchTodos') {
// Make an API call to fetch todos from the server
client.get('todos').then(todos => {
// Dispatch an action with the todos we received
storeAPI.dispatch({ type: 'todos/todosLoaded', payload: todos })
})
}
return next(action)
}
Подробнее о том, почему и как Redux использует middleware для асинхронной логики, см. в ответах создателя Redux Дэна Абрамова на StackOverflow:
Создание Middleware для асинхронных функций
Оба примера middleware из предыдущего раздела были узкоспециализированными. Было бы удобно иметь возможность заранее писать любую асинхронную логику отдельно от самого middleware, сохраняя доступ к dispatch и getState для взаимодействия с хранилищем.
Что если создать middleware, позволяющий передавать в dispatch функцию вместо объекта экшена? Наш middleware мог бы проверять, является ли "экшен" функцией, и если да — вызывать её. Это позволило бы выносить асинхронную логику в отдельные функции вне определения middleware.
Вот как может выглядеть такой middleware:
const asyncFunctionMiddleware = storeAPI => next => action => {
// If the "action" is actually a function instead...
if (typeof action === 'function') {
// then call the function and pass `dispatch` and `getState` as arguments
return action(storeAPI.dispatch, storeAPI.getState)
}
// Otherwise, it's a normal action - send it onwards
return next(action)
}
Использовать этот middleware можно так:
const middlewareEnhancer = applyMiddleware(asyncFunctionMiddleware)
const store = createStore(rootReducer, middlewareEnhancer)
// Write a function that has `dispatch` and `getState` as arguments
const fetchSomeData = (dispatch, getState) => {
// Make an async HTTP request
client.get('todos').then(todos => {
// Dispatch an action with the todos we received
dispatch({ type: 'todos/todosLoaded', payload: todos })
// Check the updated store state after dispatching
const allTodos = getState().todos
console.log('Number of todos after loading: ', allTodos.length)
})
}
// Pass the _function_ we wrote to `dispatch`
store.dispatch(fetchSomeData)
// logs: 'Number of todos after loading: ###'
Обратите внимание: этот "middleware для асинхронных функций" позволяет передавать в dispatch функцию! Внутри функции мы можем реализовать асинхронную логику (например, HTTP-запрос), а затем диспетчеризовать обычный объект экшена при завершении запроса.
Поток данных в Redux с асинхронными операциями
Как middleware и асинхронная логика влияют на общий поток данных в Redux-приложении?
Как и с обычным экшеном, сначала обрабатываем пользовательское событие (например, клик по кнопке). Затем вызываем dispatch(), передавая нечто — будь то объект экшена, функция или другое значение, которое может обработать middleware.
Когда переданное значение достигает middleware, тот может выполнить асинхронный вызов и диспетчеризовать реальный объект экшена по завершении операции.
Ранее мы видели диаграмму синхронного потока данных в Redux. При добавлении асинхронной логики появляется дополнительный этап, где middleware выполняет операции (например, HTTP-запросы) перед диспетчеризацией экшенов. Асинхронный поток данных выглядит так:

Использование Redux Thunk Middleware
Оказывается, в Redux уже есть официальная версия такого "middleware для асинхронных функций" — Redux Thunk Middleware. Thunk middleware позволяет писать функции, принимающие dispatch и getState в качестве аргументов. Thunk-функции могут содержать любую асинхронную логику и при необходимости диспетчеризовать экшены или читать состояние хранилища.
Написание асинхронной логики в виде функций thunk позволяет повторно использовать эту логику без необходимости заранее знать, какое хранилище Redux мы используем.
Слово "thunk" — это программистский термин, означающий "фрагмент кода, выполняющий отложенную работу". Подробнее об использовании thunk можно прочитать на странице руководства:
а также в этих статьях:
Настройка хранилища
Промежуточное ПО Redux thunk доступно в NPM как пакет redux-thunk. Нам нужно установить этот пакет для использования в нашем приложении:
npm install redux-thunk
После установки мы можем обновить хранилище Redux в нашем приложении для задач, чтобы использовать это промежуточное ПО:
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))
// The store now has the ability to accept thunk functions in `dispatch`
const store = createStore(rootReducer, composedEnhancer)
export default store
Получение задач с сервера
Сейчас наши задачи существуют только в браузере клиента. Нам нужен способ загружать список задач с сервера при запуске приложения.
Мы начнём с написания функции thunk, которая выполняет HTTP-запрос к нашему эндпоинту /fakeApi/todos для получения массива объектов задач, а затем диспетчеризует действие, содержащее этот массив в качестве полезной нагрузки. Поскольку это относится к функциональности задач в целом, мы напишем функцию thunk в файле todosSlice.js:
import { client } from '../../api/client'
const initialState = []
export default function todosReducer(state = initialState, action) {
// omit reducer logic
}
// Thunk function
export async function fetchTodos(dispatch, getState) {
const response = await client.get('/fakeApi/todos')
dispatch({ type: 'todos/todosLoaded', payload: response.todos })
}
Мы хотим выполнить этот вызов API только один раз, при первом запуске приложения. Есть несколько мест, куда мы могли бы это поместить:
-
В компоненте
<App>, в хукеuseEffect -
В компоненте
<TodoList>, в хукеuseEffect -
Непосредственно в файле
index.js, сразу после импорта хранилища
Пока попробуем поместить это непосредственно в index.js:
import React from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import './index.css'
import App from './App'
import './api/server'
import store from './store'
import { fetchTodos } from './features/todos/todosSlice'
store.dispatch(fetchTodos)
const root = createRoot(document.getElementById('root'))
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
)
Если мы перезагрузим страницу, видимых изменений в UI не будет. Однако, если мы откроем расширение Redux DevTools, то должны увидеть, что действие 'todos/todosLoaded' было диспетчеризовано и оно содержит некоторые объекты задач, сгенерированные нашим фейковым серверным API:

Обратите внимание, что хотя мы диспетчеризовали действие, состояние не изменилось. Нам нужно обработать это действие в нашем редюсере задач, чтобы обновить состояние.
Добавим обработку этого действия в редюсер, чтобы загрузить данные в хранилище. Поскольку мы получаем данные с сервера, мы хотим полностью заменить существующие задачи, поэтому мы можем вернуть массив action.payload, чтобы он стал новым значением state для задач:
import { client } from '../../api/client'
const initialState = []
export default function todosReducer(state = initialState, action) {
switch (action.type) {
// omit other reducer cases
case 'todos/todosLoaded': {
// Replace the existing state entirely by returning the new value
return action.payload
}
default:
return state
}
}
export async function fetchTodos(dispatch, getState) {
const response = await client.get('/fakeApi/todos')
dispatch({ type: 'todos/todosLoaded', payload: response.todos })
}
Поскольку диспетчеризация действия немедленно обновляет хранилище, мы также можем вызвать getState в thunk, чтобы прочитать обновлённое состояние после диспетчеризации. Например, мы можем вывести в консоль общее количество задач до и после диспетчеризации действия 'todos/todosLoaded':
export async function fetchTodos(dispatch, getState) {
const response = await client.get('/fakeApi/todos')
const stateBefore = getState()
console.log('Todos before dispatch: ', stateBefore.todos.length)
dispatch({ type: 'todos/todosLoaded', payload: response.todos })
const stateAfter = getState()
console.log('Todos after dispatch: ', stateAfter.todos.length)
}
Сохранение задач
Нам также необходимо обновлять сервер при попытке создать новую задачу. Вместо немедленной диспетчеризации действия 'todos/todoAdded', мы должны выполнить вызов API на сервер с исходными данными, дождаться, пока сервер вернёт копию только что сохранённой задачи, и затем диспетчеризовать действие с этой задачей.
Однако, если мы попытаемся написать эту логику как функцию thunk, мы столкнёмся с проблемой: поскольку мы пишем thunk как отдельную функцию в файле todosSlice.js, код, выполняющий вызов API, не будет знать, каким должен быть текст новой задачи:
async function saveNewTodo(dispatch, getState) {
// ❌ We need to have the text of the new todo, but where is it coming from?
const initialTodo = { text }
const response = await client.post('/fakeApi/todos', { todo: initialTodo })
dispatch({ type: 'todos/todoAdded', payload: response.todo })
}
Нам нужен способ написать функцию, которая принимает text как параметр, но затем создаёт саму thunk-функцию, чтобы использовать значение text для выполнения API-вызова. Наша внешняя функция должна возвращать thunk-функцию, чтобы мы могли передать её в dispatch в нашем компоненте.
// Write a synchronous outer function that receives the `text` parameter:
export function saveNewTodo(text) {
// And then creates and returns the async thunk function:
return async function saveNewTodoThunk(dispatch, getState) {
// ✅ Now we can use the text value and send it to the server
const initialTodo = { text }
const response = await client.post('/fakeApi/todos', { todo: initialTodo })
dispatch({ type: 'todos/todoAdded', payload: response.todo })
}
}
Теперь мы можем использовать это в компоненте <Header>:
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { saveNewTodo } from '../todos/todosSlice'
const Header = () => {
const [text, setText] = useState('')
const dispatch = useDispatch()
const handleChange = e => setText(e.target.value)
const handleKeyDown = e => {
// If the user pressed the Enter key:
const trimmedText = text.trim()
if (e.which === 13 && trimmedText) {
// Create the thunk function with the text the user wrote
const saveNewTodoThunk = saveNewTodo(trimmedText)
// Then dispatch the thunk function itself
dispatch(saveNewTodoThunk)
setText('')
}
}
// omit rendering output
}
Поскольку мы знаем, что сразу передадим thunk-функцию в dispatch в компоненте, мы можем пропустить создание временной переменной. Вместо этого мы можем вызвать saveNewTodo(text) и передать результирующую thunk-функцию напрямую в dispatch:
const handleKeyDown = e => {
// If the user pressed the Enter key:
const trimmedText = text.trim()
if (e.which === 13 && trimmedText) {
// Create the thunk function and immediately dispatch it
dispatch(saveNewTodo(trimmedText))
setText('')
}
}
Теперь компонент даже не знает, что он диспетчеризует thunk-функцию — функция saveNewTodo инкапсулирует реальную логику. Компонент <Header> знает только, что ему нужно диспетчеризовать какое-то значение при нажатии пользователем Enter.
Этот шаблон написания функции для подготовки чего-либо, что будет передано в dispatch, называется "action creator", и мы подробнее поговорим об этом в следующем разделе.
Теперь мы видим, что диспетчеризуется обновлённое действие 'todos/todoAdded':

Последнее, что нам нужно изменить, — это обновить наш редюсер todos. Когда мы делаем POST-запрос к /fakeApi/todos, сервер вернёт совершенно новый объект todo (включая новое значение ID). Это означает, что нашему редюсеру не нужно вычислять новый ID или заполнять другие поля — ему достаточно создать новый массив state, включающий новый элемент todo:
const initialState = []
export default function todosReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
// Return a new todos state array with the new todo item at the end
return [...state, action.payload]
}
// omit other cases
default:
return state
}
}
И теперь добавление нового todo будет работать корректно:

Thunk-функции можно использовать как для асинхронной, так и для синхронной логики. Thunks предоставляют способ написания любой повторно используемой логики, которой нужен доступ к dispatch и getState.
Итоги изученного
Мы успешно обновили наше приложение todo: теперь мы можем загружать список задач и сохранять новые задачи, используя "thunk"-функции для выполнения HTTP-запросов к нашему фейковому серверному API.
В процессе мы увидели, как промежуточное ПО Redux позволяет выполнять асинхронные вызовы и взаимодействовать с хранилищем, диспетчеризуя действия после завершения асинхронных операций.
Вот как выглядит текущее приложение:
- Промежуточное ПО Redux создано для реализации логики с побочными эффектами
- "Побочные эффекты" — это код, изменяющий состояние/поведение вне функции, например HTTP-запросы, модификация аргументов или генерация случайных значений
- ПО добавляет дополнительный шаг в стандартный поток данных Redux
- Может перехватывать значения, передаваемые в
dispatch - Имеет доступ к
dispatchиgetState, позволяя диспетчеризовать дополнительные действия в асинхронной логике
- Может перехватывать значения, передаваемые в
- ПО "Thunk" позволяет передавать функции в
dispatch- "Thunk"-функции позволяют заранее писать асинхронную логику без знания конкретного хранилища Redux
- Thunk-функция получает
dispatchиgetStateкак аргументы и может диспетчеризовать действия типа "эти данные получены из ответа API"
Что дальше?
Мы рассмотрели все основные аспекты использования Redux! Теперь вы знаете как:
-
Писать редюсеры, обновляющие состояние на основе диспетчеризуемых действий
-
Создавать и настраивать хранилище Redux с редюсером, расширениями и промежуточным ПО
-
Использовать промежуточное ПО для написания асинхронной логики, диспетчеризующей действия
В Части 7: Стандартные шаблоны Redux мы рассмотрим несколько паттернов кода, обычно используемых в реальных приложениях Redux, чтобы сделать наш код более согласованным и масштабируемым по мере роста приложения.