이 글은 개인 공부용으로 Redux Toolkit 공식 문서를 번역한 내용을 담고 있습니다.
useEffect()
에서 API 호출을 하는 것도 가능하다. 실제로 지금까지 그렇게 해왔다. 다만, Redux Toolkit의 비동기 기능을 사용하면, 컴포넌트 외부에서 비동기 처리를 할 수 있기 때문에 관심사 분리가 가능하다는 장점이 있다.createAsyncThunk
와 createSlice
를 사용하여 Redux Toolkit만으로 비동기 처리를 쉽게 할 수 있으며, redux-saga에서만 사용할 수 있던 기능(이미 호출한 API 요청 취소하기 등)까지 사용할 수 있다.import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
const fetchUserById = createAsyncThunk(
// string action type value: 이 값에 따라 pending, fulfilled, rejected가 붙은 액션 타입이 생성된다.
'users/fetchByIdStatus',
// payloadCreator callback: 비동기 로직의 결과를 포함하고 있는 프로미스를 반환하는 비동기 함수
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId);
return response.data;
},
// 세 번째 파라미터로 추가 옵션을 설정할 수 있다.
// condition(arg, { getState, extra } ): boolean (비동기 로직 실행 전에 취소하거나, 실행 도중에 취소할 수 있다.)
// dispatchConditionRejection: boolean (true면, condition()이 false를 반환할 때 액션 자체를 디스패치하지 않도록 한다.)
// idGenerator(): string (requestId를 만들어준다. 같은 requestId일 경우 요청하지 않는 등의 기능을 사용할 수 있게 된다.)
);
pending
액션을 디스패치한다.payloadCreator
콜백을 호출하고 프로미스가 반환되기를 기다린다.action.payload
를 fulfilled
액션에 담아 디스패치한다.rejected
액션을 디스패치하되 rejectedValue(value)
함수의 반환값에 따라 액션에 어떤 값이 넘어올지 결정된다.rejectedValue
가 값을 반환하면, action.payload
를 reject
액션에 담는다.rejectedValue
가 없거나 값을 반환하지 않았다면, action.error
값처럼 오류의 직렬화된 버전을 reject
액션에 담는다.const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add user to the state array
state.entities.push(action.payload);
})
},
});
dispatch(fetchUserById(123));
createAsyncThunk
는 결과에 상관없이 무조건 항상 이행된 프로미스를 반환한다. 따라서, 오류 처리는 별도의 방법을 사용해서 진행해야 한다.unwrap
프로퍼티를 가지고 있는데, 이를 사용해서 오류 처리를 할 수 있다.const onClick = async () => {
try {
const originalPromiseResult = await dispatch(fetchUserById(userId)).unwrap();
// handle result here
} catch (rejectedValueOrSerializedError) {
// handle error here
}
}
rejectedValue(value)
함수를 사용해서 createAsyncThunk
내부에서 오류 처리를 할 수도 있다.const updateUser = createAsyncThunk(
'users/update',
async (userData, { rejectWithValue }) => {
const { id, ...fields } = userData;
try {
const response = await userAPI.updateById(id, fields);
return response.data.user;
} catch (err) {
// Use `err.response.data` as `action.payload` for a `rejected` action,
// by explicitly returning it using the `rejectWithValue()` utility
return rejectWithValue(err.response.data);
}
}
);
createAsyncThunk
의 세 번째 파라미터(옵션)의 condition
속성을 통해 비동기 처리 전 thunk를 취소할 수 있다.condition
속성은 thunk 인자(argument)와 { getState, extra }
형식의 객체를 매개변수로 받는 함수다.condition
속성의 함수가 false
를 반환하면 thunk가 취소되며 그렇지 않을 경우 thunk는 그대로 실행된다.const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId);
return response.data;
},
{
condition: (userId, { getState, extra }) => {
const { users } = getState();
const fetchStatus = users.requests[userId];
// fetchStatus가 fulfilled이거나 fetchStatus가 loading이면 실행 취소
if (fetchStatus === 'fulfilled' || fetchStatus === 'loading') {
return false;
}
},
// 만약, thunk가 취소되더라도 rejected 액션이 디스패치되길 원한다면
// 옵션의 dispatchConditionRejection 속성을 true로 설정한다. (기본값은 false)
dispatchConditionRejection: true,
}
);
dispatch(fetchUserById(userId))
가 반환하는 abort
메소드를 사용하면 된다.import { fetchUserById } from './slice'
import { useAppDispatch } from './store'
import React from 'react'
function MyComponent(props: { userId: string }) {
const dispatch = useAppDispatch();
React.useEffect(() => {
// Dispatching the thunk returns a promise
const promise = dispatch(fetchUserById(props.userId));
return () => {
// `createAsyncThunk` attaches an `abort()` method to the promise
promise.abort();
}
}, [props.userId]);
}
payloadCreator
내부에서 두 번째 인자로 받은 thunkAPI
의 signal
을 통해 AbortSignal을 사용할 수 있다.import { createAsyncThunk } from '@reduxjs/toolkit'
const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId: string, thunkAPI) => {
const response = await fetch(`https://reqres.in/api/users/${userId}`, {
signal: thunkAPI.signal,
});
return await response.json();
}
);
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, { getState, requestId }) => {
const { currentRequestId, loading } = getState().users
if (loading !== 'pending' || requestId !== currentRequestId) {
return
}
const response = await userAPI.fetchById(userId)
return response.data
}
)
const usersSlice = createSlice({
name: 'users',
initialState: {
entities: [],
loading: 'idle',
currentRequestId: undefined,
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state, action) => {
if (state.loading === 'idle') {
state.loading = 'pending'
state.currentRequestId = action.meta.requestId
}
})
.addCase(fetchUserById.fulfilled, (state, action) => {
const { requestId } = action.meta
if (
state.loading === 'pending' &&
state.currentRequestId === requestId
) {
state.loading = 'idle'
state.entities.push(action.payload)
state.currentRequestId = undefined
}
})
.addCase(fetchUserById.rejected, (state, action) => {
const { requestId } = action.meta
if (
state.loading === 'pending' &&
state.currentRequestId === requestId
) {
state.loading = 'idle'
state.error = action.error
state.currentRequestId = undefined
}
})
},
})
const UsersComponent = () => {
const { users, loading, error } = useSelector((state) => state.users)
const dispatch = useDispatch()
const fetchOneUser = async (userId) => {
try {
const user = await dispatch(fetchUserById(userId)).unwrap()
showToast('success', `Fetched ${user.name}`)
} catch (err) {
showToast('error', `Fetch failed: ${err.message}`)
}
}
// render UI here
}
createAsyncThunk | Redux Toolkit
redux toolkit createAsyncThunk
구글링을 많이 했는데 가장 간단하게 알려주세요 감사합니다