0Pricing
React Native Academy · 课时

使用 createAsyncThunk 处理异步任务

在 createAsyncThunk 中从 API 获取数据,在切片的 extraReducers 中处理待定、成功和失败的生命周期,并在界面中显示异步状态。

使用 createAsyncThunk 处理异步任务 是 CoddyKit 上的免费 React Native Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 React Native Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 React Native Academy 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Why Do We Need Async Thunks?

Redux reducers must be pure synchronous functions — they cannot make API calls or run async code. To fetch data from a server and store the result in Redux, we need an async thunk: a function that dispatches plain actions before and after an async operation. Redux Toolkit's createAsyncThunk handles this pattern with minimal boilerplate.

Creating an Async Thunk

createAsyncThunk accepts two arguments: a string action type prefix and a payload creator function that performs the async work and returns the result. It automatically dispatches pending, fulfilled, or rejected actions based on whether the payload creator's promise resolves or rejects.

import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

export const fetchPosts = createAsyncThunk(
  'posts/fetchPosts',          // action type prefix
  async (page: number) => {    // payload creator
    const response = await axios.get('https://api.example.com/posts?page=' + page);
    return response.data;       // this becomes action.payload
  }
);

The Three Lifecycle Actions

For every createAsyncThunk call, RTK automatically creates three action creators: thunk.pending (dispatched when the async call starts), thunk.fulfilled (dispatched with the returned data when it succeeds), and thunk.rejected (dispatched with the error when it fails). You handle each case in extraReducers.

// These are created automatically:
fetchPosts.pending.type   // 'posts/fetchPosts/pending'
fetchPosts.fulfilled.type // 'posts/fetchPosts/fulfilled'
fetchPosts.rejected.type  // 'posts/fetchPosts/rejected'

Handling Lifecycle in extraReducers

Use the builder.addCase pattern inside extraReducers to handle each lifecycle action. In the pending case set a loading flag, in fulfilled store the data and clear loading, and in rejected store the error message and clear loading. This gives every async operation a consistent state machine.

const postsSlice = createSlice({
  name: 'posts',
  initialState: { items: [], status: 'idle', error: null },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchPosts.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchPosts.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message ?? 'Unknown error';
      });
  },
});

Dispatching an Async Thunk from a Component

Dispatch an async thunk exactly like a regular action — pass it to dispatch(). The thunk returns a Promise, so you can await it or chain .then() if you need to react after the async operation completes inside the component. Use useEffect to dispatch it on mount.

import { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { fetchPosts } from './postsSlice';

export default function PostsScreen() {
  const dispatch = useAppDispatch();
  const { items, status, error } = useAppSelector((s) => s.posts);

  useEffect(() => {
    dispatch(fetchPosts(1));
  }, [dispatch]);

  if (status === 'loading') return <ActivityIndicator />;
  if (status === 'failed') return <Text>{error}</Text>;
  return <FlatList data={items} /* ... */ />;
}

Passing Arguments to the Thunk

The payload creator function receives the first argument passed to the thunk when dispatched, and a second argument called thunkAPI that provides getState, dispatch, rejectWithValue, and more. Use getState to read current Redux state inside the thunk, for example to include an auth token in a request header.

export const createPost = createAsyncThunk(
  'posts/create',
  async (postData: { title: string; body: string }, thunkAPI) => {
    const state = thunkAPI.getState() as RootState;
    const token = state.user.token;
    const response = await axios.post('/posts', postData, {
      headers: { Authorization: 'Bearer ' + token },
    });
    return response.data;
  }
);

Handling Errors with rejectWithValue

By default, the rejected action contains only the serialized error message. If you need to pass a custom error payload — such as an HTTP status code or server-provided error message — use thunkAPI.rejectWithValue() inside a try-catch block. The value you pass becomes action.payload in the rejected case.

export const fetchUser = createAsyncThunk(
  'user/fetch',
  async (userId: string, { rejectWithValue }) => {
    try {
      const response = await axios.get('/users/' + userId);
      return response.data;
    } catch (err: any) {
      return rejectWithValue(err.response.data.message);
    }
  }
);

// In extraReducers:
.addCase(fetchUser.rejected, (state, action) => {
  state.error = action.payload as string; // custom error message
})

Unwrapping the Dispatch Result

Dispatching a thunk returns a special Promise that you can unwrap() to get the fulfilled value or throw the error. This is useful when you need the result directly in the component — for example, navigating to a new screen after a successful API call without storing transient navigation state in Redux.

async function handleSubmit() {
  try {
    const result = await dispatch(createPost({ title, body })).unwrap();
    navigation.navigate('PostDetail', { id: result.id });
  } catch (err) {
    Alert.alert('Error', 'Failed to create post');
  }
}

Cancelling an In-Flight Request

createAsyncThunk supports request cancellation via an AbortController. The thunkAPI.signal is an AbortSignal that you can pass to Axios or Fetch. When the component unmounts or the user navigates away, calling the thunk's abort() method cancels the pending request and dispatches the rejected action with AbortError.

export const fetchData = createAsyncThunk(
  'data/fetch',
  async (_, { signal }) => {
    const response = await axios.get('/data', { signal });
    return response.data;
  }
);

// In component useEffect:
useEffect(() => {
  const promise = dispatch(fetchData());
  return () => promise.abort(); // cancel on unmount
}, []);

Status-Driven UI Patterns

The status field in your slice state acts as a state machine with four values: idle, loading, succeeded, and failed. Pattern-match on this value in your component to show the right UI for each state. This prevents showing stale data and empty lists simultaneously, giving users clear feedback throughout the async lifecycle.

function renderContent() {
  switch (status) {
    case 'idle':
      return <Text>Press button to load</Text>;
    case 'loading':
      return <ActivityIndicator size='large' />;
    case 'succeeded':
      return <FlatList data={items} renderItem={renderItem} />;
    case 'failed':
      return <Text style={styles.error}>{error}</Text>;
  }
}

Multiple Thunks for CRUD Operations

Real apps typically need separate thunks for each HTTP verb. Create one thunk per operation — fetchItems, addItem, updateItem, deleteItem — and handle each in extraReducers. After a successful mutation, update the local items array in the fulfilled handler so the UI stays in sync without re-fetching the entire list.

.addCase(deleteItem.fulfilled, (state, action) => {
  state.items = state.items.filter((item) => item.id !== action.payload);
})
.addCase(addItem.fulfilled, (state, action) => {
  state.items.push(action.payload);
})
.addCase(updateItem.fulfilled, (state, action) => {
  const index = state.items.findIndex((i) => i.id === action.payload.id);
  if (index !== -1) state.items[index] = action.payload;
})

Quick Check

Test your understanding of React Native Mobile Development concepts from this lesson.

Lesson Recap

In this lesson you learned: createAsyncThunk wraps async operations and dispatches pending/fulfilled/rejected lifecycle actions automatically, extraReducers with builder.addCase handles each lifecycle to update loading and error state, and rejectWithValue lets you pass custom error data to the rejected handler. Next up we explore Zustand, a lightweight alternative to Redux.

常见问题解答

「使用 createAsyncThunk 处理异步任务」课时是免费的吗?

是的 — 「使用 createAsyncThunk 处理异步任务」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 React Native Academy 课程的其余内容,请升级到 CoddyKit PRO。 React Native Academy 课程共包含 4 节课。

「使用 createAsyncThunk 处理异步任务」这节课中我会学到什么?

在 createAsyncThunk 中从 API 获取数据,在切片的 extraReducers 中处理待定、成功和失败的生命周期,并在界面中显示异步状态。 你通过在浏览器中直接运行的动手代码来练习 React Native Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 React Native Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 React Native Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「使用 createAsyncThunk 处理异步任务」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 React Native Academy 课中编写并运行代码吗?

能。每节 React Native Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 设置 Redux 存储与 Provider
  2. 使用 createSlice 创建切片
  3. 使用 useSelector 读取状态
  4. 使用 createAsyncThunk 处理异步任务
← 返回 React Native Academy