'How to use createAsyncThunk with Typescript? How to set types for the `pending` and `rejected` payloads?
Right now I've got these actions that I use for an upload thunk's lifecycle.
type UPLOAD_START = PayloadAction<void>
type UPLOAD_SUCCESS = PayloadAction<{ src: string, sizeKb: number }>
type UPLOAD_FAILURE = PayloadAction<{ error: string }>
And I'd like to convert it to a createAsyncThunk call, assuming it will reduce the code. But will it?
From the example on https://redux-toolkit.js.org/api/createAsyncThunk it should be something like:
const uploadThumbnail = createAsyncThunk(
'mySlice/uploadThumbnail',
async (file: File, thunkAPI) => {
const response = await uploadAPI.upload(file) as API_RESPONSE
return response.data // IS THIS THE payload FOR THE fulfilled ACTION ?
}
)
This is how I would handle the life cycle actions?
const usersSlice = createSlice({
name: 'mySlice',
initialState: // SOME INITIAL STATE,
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: {
// Add reducers for additional action types here, and handle loading state as needed
[uploadThumbnail.pending]: (state,action) => {
// HANDLE MY UPLOAD_START ACTION
},
[uploadThumbnail.fulfilled]: (state, action) => {
// HANDLE MY UPLOAD_SUCCESS ACTION
},
[uploadThumbnail.rejected]: (state, action) => {
// HANDLE MY UPLOAD_FAILURE ACTION
},
}
})
QUESTION
I'm assuming the return of the createAsyncThunk async handler is the payload for the fulfilled action, is that right?
But how can I set the payload types for the pending and the rejected actions? Should I add a try-catch block to the createAsyncThunk handler?
Is this the correlation I should be doing?
pending === "UPLOAD_START"fulfilled === "UPLOAD_SUCCESS"rejected === "UPLOAD_FAILURE"
Obs: From the pattern I'm imagining, it doesn't look I'll be writing any less code than what I'm already doing with three separate actions and handling them in my regular reducers (instead of doing it on the extraReducers prop). What is the point of using the createAsyncThunk in this case?
Solution 1:[1]
Most of your questions will be answered by looking at one of the TypeScript examples a little further down in the docs page you linked:
export const updateUser = createAsyncThunk<
User,
{ id: string } & Partial<User>,
{
rejectValue: ValidationErrors
}
>('users/update', async (userData, { rejectWithValue }) => {
try {
const { id, ...fields } = userData
const response = await userAPI.updateById<UpdateUserResponse>(id, fields)
return response.data.user
} catch (err) {
let error: AxiosError<ValidationErrors> = err // cast the error for access
if (!error.response) {
throw err
}
// We got validation errors, let's return those so we can reference in our component and set form errors
return rejectWithValue(error.response.data)
}
})
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: (builder) => {
// The `builder` callback form is used here because it provides correctly typed reducers from the action creators
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
state.entities[payload.id] = payload
})
builder.addCase(updateUser.rejected, (state, action) => {
if (action.payload) {
// Being that we passed in ValidationErrors to rejectType in `createAsyncThunk`, the payload will be available here.
state.error = action.payload.errorMessage
} else {
state.error = action.error.message
}
})
},
})
So, observations from there:
- when using TypeScript, you should use the
builderstyle notation for extraReducers and all your Types will be automatically inferred for you. You should not need to type anything down inextraReducersby hand - ever. - the
returned value of your thunk will be thepayloadof the "fulfilled" action - if you
return rejectWithResult(value), that will become thepayloadof the "rejected" action - if you just
throw, that will become theerrorof the "rejected" action.
Additional answers:
- "pending" is your "UPLOAD_START". It does not have a payload and you cannot set it.
All three of "pending"/"rejected"/"fulfilled" will have
action.meta.argthough, which is the original value you passed into the thunk call. - in the end, this is probably a little less code than you would write from hand, and it will be very consistent throughout your application. Also, it catches some bugs that would go unseen otherwise. Do you know that
const manualThunk = async (arg) => {
dispatch(pendingAction())
try {
const result = await foo(arg)
dispatch(successAction(result))
} catch (e) {
dispatch(errorAction(e))
}
}
actually contains a bug?
If successAction triggers a rerender (which it most likely does) and somewhere during that rerender, an error is thrown, that error will be caught in this try..catch block and another errorAction will be dispatched. So you will have a thunk with both the success and error case true at the same time. Awkward. This can be circumvented by storing the result in a scoped-up variable and dispatching outside of the try-catch-block, but who does that in reality? ;)
It's these little things that createAsyncThunk takes care of for you that make it worth it in my eyes.
Solution 2:[2]
Type 'unknown' is not assignable to type 'AxiosError<KnownError, any>
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | phry |
| Solution 2 | Say0 |
