Обработка асинхронных операций в Redux требует специальных подходов, так как редьюсеры должны оставаться чистыми функциями. Рассмотрим основные методы и лучшие практики.
Популярное решение для простых асинхронных операций
npm install redux-thunk
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(rootReducer, applyMiddleware(thunk));
const fetchProducts = () => {
return async (dispatch) => {
dispatch({ type: 'PRODUCTS_REQUEST' });
try {
const response = await fetch('/api/products');
const data = await response.json();
dispatch({ type: 'PRODUCTS_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'PRODUCTS_FAILURE', error: error.message });
}
};
};
// Использование:
dispatch(fetchProducts());
Современный рекомендуемый подход
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
export const fetchUserData = createAsyncThunk(
'users/fetchById',
async (userId, { rejectWithValue }) => {
try {
const response = await fetch(`/api/users/${userId}`);
return await response.json();
} catch (err) {
return rejectWithValue(err.response.data);
}
}
);
const usersSlice = createSlice({
name: 'users',
initialState: { data: null, status: 'idle', error: null },
extraReducers: (builder) => {
builder
.addCase(fetchUserData.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchUserData.fulfilled, (state, action) => {
state.status = 'succeeded';
state.data = action.payload;
})
.addCase(fetchUserData.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
});
}
});
Использует генераторы для управления side-эффектами
npm install redux-saga
import { call, put, takeEvery } from 'redux-saga/effects';
function* fetchUser(action) {
try {
const user = yield call(fetch, `/api/users/${action.payload}`);
yield put({ type: 'USER_FETCH_SUCCEEDED', payload: user });
} catch (e) {
yield put({ type: 'USER_FETCH_FAILED', error: e.message });
}
}
function* mySaga() {
yield takeEvery('USER_FETCH_REQUESTED', fetchUser);
}
// Подключение saga middleware
import createSagaMiddleware from 'redux-saga';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(mySaga);
Встроенный data-fetching инструмент в Redux Toolkit
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getProducts: builder.query({
query: () => '/products',
}),
}),
});
// Использование в компоненте
const { data, error, isLoading } = useGetProductsQuery();
Метод | Сложность | Использование |
---|---|---|
Redux Thunk | Низкая | Простые асинхронные операции |
createAsyncThunk | Средняя | Стандартные API запросы |
Redux Saga | Высокая | Сложные workflows, отмена запросов |
RTK Query | Низкая | Data fetching, кэширование |
Три состояния запроса:
Нормализация данных:
{
ids: [1, 2, 3],
entities: {
1: {id: 1, name: 'Product 1'},
2: {id: 2, name: 'Product 2'}
}
}
Оптимистичные обновления:
// Redux Toolkit позволяет мутировать состояние в редьюсерах
addCase(updateProduct.fulfilled, (state, action) => {
const { id, ...changes } = action.payload;
state.entities[id] = { ...state.entities[id], ...changes };
}
Для простых случаев:
createAsyncThunk
из Redux Toolkitredux-thunk
Для сложных сценариев:
redux-saga
для сложных асинхронных workflowsRTK Query
для data fetchingРекомендации:
Пример обработки загрузки в компоненте:
function ProductsList() {
const { data: products, isLoading, error } = useSelector(state => state.products);
if (isLoading) return <Spinner />;
if (error) return <Error message={error} />;
return products.map(product => <ProductItem key={product.id} {...product} />);
}