I am new to redux and redux toolkit. I am reading this tutorial from redux toolkit official document. https://redux-toolkit.js.org/api/createAsyncThunk. This is an example it provides
import { createAsyncThunk, createSlice, unwrapResult } 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: {
[fetchUserById.pending]: (state, action) => {
if (state.loading === 'idle') {
state.loading = 'pending'
state.currentRequestId = action.meta.requestId
}
},
[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
}
},
[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 resultAction = await dispatch(fetchUserById(userId))
const user = unwrapResult(resultAction)
showToast('success', `Fetched ${user.name}`)
} catch (err) {
showToast('error', `Fetch failed: ${err.message}`)
}
}
// render UI here
}
my question is, since we have handled the case where the promise rejects in the lifecycle action creator i.e.
[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
}
}
Then why is that, in UsersComponent, we still need to try and catch the error thrown by the dispatch.Can't we tell if the request failed or not from the error state? To be clearer, what about I write the UsersComponent this following way:
const UsersComponent = () => {
const { users, loading, error } = useSelector(state => state.users)
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchUserById(userId))
},[dispatch])
if(error) {
showToast('error', `Fetch failed: ${error.message}`)
} else if(loading) {
showToast('loading')
} else {
showToast('success', `Fetched ${user.name}`)
}
// render UI here
}
Additionally, I wonder when the fetchUserById.rejected would be dispatched? According to the doc, createAsyncThunk will always return a resolved promise. If we hit this condition in the callback of createAsyncThunk
if (loading !== 'pending' || requestId !== currentRequestId) {
return
}
then the lifecycle action gets dispatched would be fulfilled right? What does it take for the rejected lifecyle action gets dispatched? Only when an exception is thrown inside of the callback?
My last question is, if I'd like to check if the data I got back is of the right shape, for example, I want to make sure if has a property called data on it. How can I achieve that? If I rewrite the callback in createAsyncThunk to be
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)
if(!response.data) throw new Error('No data')
return response.data
}
)
will the rejected lifecycle action gets dispatched when there is actually no data property on the response?