Redux сам по себе синхронен. Для асинхронных операций (API запросы, таймеры) нужен middleware. Redux Toolkit включает thunk middleware по умолчанию.
Thunk — это функция, которая возвращает другую функцию. В контексте Redux это позволяет dispatch'ить функции вместо объектов:
// Обычный action — объект
dispatch({ type: 'counter/increment' })
// Thunk — функция
dispatch((dispatch, getState) => {
// Можем делать асинхронные операции
setTimeout(() => {
dispatch({ type: 'counter/increment' })
}, 1000)
})RTK предоставляет createAsyncThunk для упрощения асинхронных операций:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
// Создаём async thunk
const fetchUsers = createAsyncThunk(
'users/fetchAll', // action type prefix
async (_, thunkAPI) => {
const response = await fetch('/api/users')
if (!response.ok) {
return thunkAPI.rejectWithValue('Ошибка загрузки')
}
return response.json() // Это станет action.payload
}
)
// Использование
dispatch(fetchUsers())createAsyncThunk автоматически создаёт 3 action types:
| Action | Когда | payload |
|--------|-------|---------|
| users/fetchAll/pending | Запрос начался | undefined |
| users/fetchAll/fulfilled | Успех | Результат |
| users/fetchAll/rejected | Ошибка | Error |
const usersSlice = createSlice({
name: 'users',
initialState: {
items: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null
},
reducers: {
// Синхронные редьюсеры
},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.status = 'loading'
state.error = null
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded'
state.items = action.payload
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed'
state.error = action.payload || action.error.message
})
}
})const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId, thunkAPI) => {
const response = await fetch(`/api/users/${userId}`)
return response.json()
}
)
// Вызов с параметром
dispatch(fetchUserById(123))const fetchPosts = createAsyncThunk(
'posts/fetch',
async (_, { getState, dispatch, rejectWithValue, signal }) => {
// getState() — текущее состояние store
const { auth } = getState()
if (!auth.token) {
return rejectWithValue('Не авторизован')
}
// signal — для отмены запроса (AbortController)
const response = await fetch('/api/posts', {
headers: { Authorization: `Bearer ${auth.token}` },
signal
})
if (!response.ok) {
return rejectWithValue(await response.text())
}
return response.json()
}
)// В компоненте
useEffect(() => {
const promise = dispatch(fetchUsers())
return () => {
promise.abort() // Отменяем при unmount
}
}, [dispatch])function UsersList() {
const dispatch = useDispatch()
const { items, status, error } = useSelector(state => state.users)
useEffect(() => {
if (status === 'idle') {
dispatch(fetchUsers())
}
}, [status, dispatch])
if (status === 'loading') return <Spinner />
if (status === 'failed') return <Error message={error} />
return (
<ul>
{items.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
)
}const createPost = createAsyncThunk(
'posts/create',
async (postData, { dispatch }) => {
const response = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(postData)
})
const newPost = await response.json()
// После создания поста — обновляем список
dispatch(fetchPosts())
return newPost
}
)Полный пример: загрузка данных с createAsyncThunk
// Симуляция createAsyncThunk
function createAsyncThunk(typePrefix, payloadCreator) {
const pending = typePrefix + '/pending'
const fulfilled = typePrefix + '/fulfilled'
const rejected = typePrefix + '/rejected'
const actionCreator = (arg) => {
return async (dispatch, getState) => {
dispatch({ type: pending })
try {
const result = await payloadCreator(arg, { getState, dispatch })
dispatch({ type: fulfilled, payload: result })
return { payload: result }
} catch (error) {
dispatch({ type: rejected, payload: error.message })
return { error: error.message }
}
}
}
actionCreator.pending = pending
actionCreator.fulfilled = fulfilled
actionCreator.rejected = rejected
return actionCreator
}
// Симуляция API
const fakeApi = {
async getUsers() {
await new Promise(r => setTimeout(r, 1000))
return [
{ id: 1, name: 'Алексей', email: 'alex@test.com' },
{ id: 2, name: 'Мария', email: 'maria@test.com' },
{ id: 3, name: 'Иван', email: 'ivan@test.com' },
]
},
async deleteUser(id) {
await new Promise(r => setTimeout(r, 500))
return { success: true, id }
}
}
// Создаём async thunks
const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
console.log('📡 Загружаем пользователей...')
return await fakeApi.getUsers()
})
const deleteUser = createAsyncThunk('users/delete', async (userId) => {
console.log('🗑️ Удаляем пользователя:', userId)
return await fakeApi.deleteUser(userId)
})
// Начальное состояние
let state = {
users: {
items: [],
status: 'idle',
error: null
}
}
// Редьюсер с extraReducers логикой
function usersReducer(state, action) {
switch (action.type) {
case fetchUsers.pending:
return { ...state, status: 'loading', error: null }
case fetchUsers.fulfilled:
return { ...state, status: 'succeeded', items: action.payload }
case fetchUsers.rejected:
return { ...state, status: 'failed', error: action.payload }
case deleteUser.fulfilled:
return {
...state,
items: state.items.filter(u => u.id !== action.payload.id)
}
default:
return state
}
}
// Симуляция dispatch с поддержкой thunks
function dispatch(action) {
if (typeof action === 'function') {
return action(dispatch, () => state)
}
console.log('⚡ Action:', action.type)
state.users = usersReducer(state.users, action)
console.log('📊 State:', state.users.status, '| Users:', state.users.items.length)
return action
}
// === Тест ===
async function main() {
console.log('=== createAsyncThunk Demo ===\n')
await dispatch(fetchUsers())
console.log('\nПользователи загружены:', state.users.items.map(u => u.name))
await dispatch(deleteUser(2))
console.log('\nПосле удаления:', state.users.items.map(u => u.name))
}
main()Redux сам по себе синхронен. Для асинхронных операций (API запросы, таймеры) нужен middleware. Redux Toolkit включает thunk middleware по умолчанию.
Thunk — это функция, которая возвращает другую функцию. В контексте Redux это позволяет dispatch'ить функции вместо объектов:
// Обычный action — объект
dispatch({ type: 'counter/increment' })
// Thunk — функция
dispatch((dispatch, getState) => {
// Можем делать асинхронные операции
setTimeout(() => {
dispatch({ type: 'counter/increment' })
}, 1000)
})RTK предоставляет createAsyncThunk для упрощения асинхронных операций:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
// Создаём async thunk
const fetchUsers = createAsyncThunk(
'users/fetchAll', // action type prefix
async (_, thunkAPI) => {
const response = await fetch('/api/users')
if (!response.ok) {
return thunkAPI.rejectWithValue('Ошибка загрузки')
}
return response.json() // Это станет action.payload
}
)
// Использование
dispatch(fetchUsers())createAsyncThunk автоматически создаёт 3 action types:
| Action | Когда | payload |
|--------|-------|---------|
| users/fetchAll/pending | Запрос начался | undefined |
| users/fetchAll/fulfilled | Успех | Результат |
| users/fetchAll/rejected | Ошибка | Error |
const usersSlice = createSlice({
name: 'users',
initialState: {
items: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null
},
reducers: {
// Синхронные редьюсеры
},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.status = 'loading'
state.error = null
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded'
state.items = action.payload
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed'
state.error = action.payload || action.error.message
})
}
})const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId, thunkAPI) => {
const response = await fetch(`/api/users/${userId}`)
return response.json()
}
)
// Вызов с параметром
dispatch(fetchUserById(123))const fetchPosts = createAsyncThunk(
'posts/fetch',
async (_, { getState, dispatch, rejectWithValue, signal }) => {
// getState() — текущее состояние store
const { auth } = getState()
if (!auth.token) {
return rejectWithValue('Не авторизован')
}
// signal — для отмены запроса (AbortController)
const response = await fetch('/api/posts', {
headers: { Authorization: `Bearer ${auth.token}` },
signal
})
if (!response.ok) {
return rejectWithValue(await response.text())
}
return response.json()
}
)// В компоненте
useEffect(() => {
const promise = dispatch(fetchUsers())
return () => {
promise.abort() // Отменяем при unmount
}
}, [dispatch])function UsersList() {
const dispatch = useDispatch()
const { items, status, error } = useSelector(state => state.users)
useEffect(() => {
if (status === 'idle') {
dispatch(fetchUsers())
}
}, [status, dispatch])
if (status === 'loading') return <Spinner />
if (status === 'failed') return <Error message={error} />
return (
<ul>
{items.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
)
}const createPost = createAsyncThunk(
'posts/create',
async (postData, { dispatch }) => {
const response = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(postData)
})
const newPost = await response.json()
// После создания поста — обновляем список
dispatch(fetchPosts())
return newPost
}
)Полный пример: загрузка данных с createAsyncThunk
// Симуляция createAsyncThunk
function createAsyncThunk(typePrefix, payloadCreator) {
const pending = typePrefix + '/pending'
const fulfilled = typePrefix + '/fulfilled'
const rejected = typePrefix + '/rejected'
const actionCreator = (arg) => {
return async (dispatch, getState) => {
dispatch({ type: pending })
try {
const result = await payloadCreator(arg, { getState, dispatch })
dispatch({ type: fulfilled, payload: result })
return { payload: result }
} catch (error) {
dispatch({ type: rejected, payload: error.message })
return { error: error.message }
}
}
}
actionCreator.pending = pending
actionCreator.fulfilled = fulfilled
actionCreator.rejected = rejected
return actionCreator
}
// Симуляция API
const fakeApi = {
async getUsers() {
await new Promise(r => setTimeout(r, 1000))
return [
{ id: 1, name: 'Алексей', email: 'alex@test.com' },
{ id: 2, name: 'Мария', email: 'maria@test.com' },
{ id: 3, name: 'Иван', email: 'ivan@test.com' },
]
},
async deleteUser(id) {
await new Promise(r => setTimeout(r, 500))
return { success: true, id }
}
}
// Создаём async thunks
const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
console.log('📡 Загружаем пользователей...')
return await fakeApi.getUsers()
})
const deleteUser = createAsyncThunk('users/delete', async (userId) => {
console.log('🗑️ Удаляем пользователя:', userId)
return await fakeApi.deleteUser(userId)
})
// Начальное состояние
let state = {
users: {
items: [],
status: 'idle',
error: null
}
}
// Редьюсер с extraReducers логикой
function usersReducer(state, action) {
switch (action.type) {
case fetchUsers.pending:
return { ...state, status: 'loading', error: null }
case fetchUsers.fulfilled:
return { ...state, status: 'succeeded', items: action.payload }
case fetchUsers.rejected:
return { ...state, status: 'failed', error: action.payload }
case deleteUser.fulfilled:
return {
...state,
items: state.items.filter(u => u.id !== action.payload.id)
}
default:
return state
}
}
// Симуляция dispatch с поддержкой thunks
function dispatch(action) {
if (typeof action === 'function') {
return action(dispatch, () => state)
}
console.log('⚡ Action:', action.type)
state.users = usersReducer(state.users, action)
console.log('📊 State:', state.users.status, '| Users:', state.users.items.length)
return action
}
// === Тест ===
async function main() {
console.log('=== createAsyncThunk Demo ===\n')
await dispatch(fetchUsers())
console.log('\nПользователи загружены:', state.users.items.map(u => u.name))
await dispatch(deleteUser(2))
console.log('\nПосле удаления:', state.users.items.map(u => u.name))
}
main()Создай приложение для загрузки постов с API. Реализуй состояния loading, success, error. Добавь кнопку "Повторить" при ошибке и автоматическую загрузку при монтировании.
loading: status: "loading". succeeded: status: "succeeded". failed: status: "failed", error: error.message.