이 글은 개인 공부용으로 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
구글링을 많이 했는데 가장 간단하게 알려주세요 감사합니다