createAsyncThunk를 사용한 비동기 썽크
createAsyncThunk 안에서 API 데이터를 가져오고, 슬라이스의 extraReducers에서 대기 중·성공·실패 수명 주기를 처리하며, UI에 비동기 상태를 표시합니다.
createAsyncThunk를 사용한 비동기 썽크은(는) CoddyKit의 무료 React Native Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 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를 사용한 비동기 썽크” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 React Native Academy 강의 전체를 잠금 해제할 수 있습니다. React Native Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“createAsyncThunk를 사용한 비동기 썽크”에서 뭘 배우나요?
createAsyncThunk 안에서 API 데이터를 가져오고, 슬라이스의 extraReducers에서 대기 중·성공·실패 수명 주기를 처리하며, UI에 비동기 상태를 표시합니다. 브라우저에서 직접 실행하는 실습 코드로 React Native Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
React Native Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 React Native Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“createAsyncThunk를 사용한 비동기 썽크” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 React Native Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 React Native Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- Redux 저장소와 Provider 설정
- createSlice로 슬라이스 만들기
- useSelector로 상태 읽기
- createAsyncThunk를 사용한 비동기 썽크