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?
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:
builder
style notation for extraReducers and all your Types will be automatically inferred for you. You should not need to type anything down in extraReducers
by hand - ever.return
ed value of your thunk will be the payload
of the "fulfilled" actionreturn rejectWithResult(value)
, that will become the payload
of the "rejected" actionthrow
, that will become the error
of the "rejected" action.Additional answers:
action.meta.arg
though, which is the original value you passed into the thunk call. 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 throw
n, 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.