← React/Redux Toolkit: createAsyncThunk#303 из 383← ПредыдущийСледующий →+30 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksТема: React: хуки и экосистема

Redux Toolkit: createAsyncThunk

Асинхронные операции в Redux

Redux сам по себе синхронен. Для асинхронных операций (API запросы, таймеры) нужен middleware. Redux Toolkit включает thunk middleware по умолчанию.

Что такое Thunk

Thunk — это функция, которая возвращает другую функцию. В контексте Redux это позволяет dispatch'ить функции вместо объектов:

// Обычный action — объект
dispatch({ type: 'counter/increment' })

// Thunk — функция
dispatch((dispatch, getState) => {
  // Можем делать асинхронные операции
  setTimeout(() => {
    dispatch({ type: 'counter/increment' })
  }, 1000)
})

createAsyncThunk

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())

Три состояния async thunk

createAsyncThunk автоматически создаёт 3 action types:

| Action | Когда | payload |

|--------|-------|---------|

| users/fetchAll/pending | Запрос начался | undefined |

| users/fetchAll/fulfilled | Успех | Результат |

| users/fetchAll/rejected | Ошибка | Error |

extraReducers — обработка async actions

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
      })
  }
})

Передача параметров в thunk

const fetchUserById = createAsyncThunk(
  'users/fetchById',
  async (userId, thunkAPI) => {
    const response = await fetch(`/api/users/${userId}`)
    return response.json()
  }
)

// Вызов с параметром
dispatch(fetchUserById(123))

thunkAPI — доступ к store и отмена

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>
  )
}

Цепочки async операций

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 Toolkit: createAsyncThunk

Асинхронные операции в Redux

Redux сам по себе синхронен. Для асинхронных операций (API запросы, таймеры) нужен middleware. Redux Toolkit включает thunk middleware по умолчанию.

Что такое Thunk

Thunk — это функция, которая возвращает другую функцию. В контексте Redux это позволяет dispatch'ить функции вместо объектов:

// Обычный action — объект
dispatch({ type: 'counter/increment' })

// Thunk — функция
dispatch((dispatch, getState) => {
  // Можем делать асинхронные операции
  setTimeout(() => {
    dispatch({ type: 'counter/increment' })
  }, 1000)
})

createAsyncThunk

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())

Три состояния async thunk

createAsyncThunk автоматически создаёт 3 action types:

| Action | Когда | payload |

|--------|-------|---------|

| users/fetchAll/pending | Запрос начался | undefined |

| users/fetchAll/fulfilled | Успех | Результат |

| users/fetchAll/rejected | Ошибка | Error |

extraReducers — обработка async actions

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
      })
  }
})

Передача параметров в thunk

const fetchUserById = createAsyncThunk(
  'users/fetchById',
  async (userId, thunkAPI) => {
    const response = await fetch(`/api/users/${userId}`)
    return response.json()
  }
)

// Вызов с параметром
dispatch(fetchUserById(123))

thunkAPI — доступ к store и отмена

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>
  )
}

Цепочки async операций

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.

Загружаем среду выполнения...
Загружаем AI-помощника...