Skip to content

Commit

Permalink
Improve doc clarity around TypeScript and createAsyncThunk usage
Browse files Browse the repository at this point in the history
I had a difficult time figuring out how to get proper TypeScript responses for dispatched thunk actions. I read the docs several times, and even though the answer was there (use type narrowing via match), I missed it each time.

I think I skipped it because the relevant information was nested under the "Manually defining thunkAPI types" category, which I wasn't doing in my case.

Since this type narrowing is very useful and relevant even when you're not manually typing `thunkAPI`, I'm making a proposal in this PR to move this information slightly higher in the doc and give it a dedicated section.

For more context, see my question [on Discord](https://discord.com/channels/102860784329052160/103538784460615680/1264479048524890224).

I hope you'll consider making this small change. Thank you!
  • Loading branch information
EvHaus authored Jul 21, 2024
1 parent a9362fb commit cba147f
Showing 1 changed file with 50 additions and 48 deletions.
98 changes: 50 additions & 48 deletions docs/usage/usage-with-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,56 @@ const fetchUserById = createAsyncThunk(
const lastReturnedAction = await store.dispatch(fetchUserById(3))
```

### Handling responses from async thunks

You can leverage checks against `action.payload` and `match` as provided by `createAction` as a type-guard for when you want to access known properties on defined types. Example:

#### In a reducer

```ts
const usersSlice = createSlice({
name: 'users',
initialState: {
entities: {},
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
state.entities[payload.id] = payload
})
builder.addCase(updateUser.rejected, (state, action) => {
if (action.payload) {
// Since we passed in `MyKnownError` to `rejectValue` in `updateUser`, the type information will be available here.
state.error = action.payload.errorMessage
} else {
state.error = action.error
}
})
},
})
```

#### In a component

```ts
const handleUpdateUser = async (userData) => {
const resultAction = await dispatch(updateUser(userData))
if (updateUser.fulfilled.match(resultAction)) {
const user = resultAction.payload
showToast('success', `Updated ${user.name}`)
} else {
if (resultAction.payload) {
// Since we passed in `MyKnownError` to `rejectValue` in `updateUser`, the type information will be available here.
// Note: this would also be a good place to do any handling that relies on the `rejectedWithValue` payload, such as setting field errors
showToast('error', `Update failed: ${resultAction.payload.errorMessage}`)
} else {
showToast('error', `Update failed: ${resultAction.error.message}`)
}
}
}
```

### Typing the `thunkApi` Object

The second argument to the `payloadCreator`, known as `thunkApi`, is an object containing references to the `dispatch`, `getState`, and `extra` arguments from the thunk middleware as well as a utility function called `rejectWithValue`. If you want to use these from within the `payloadCreator`, you will need to define some generic arguments, as the types for these arguments cannot be inferred. Also, as TS cannot mix explicit and inferred generic parameters, from this point on you'll have to define the `Returned` and `ThunkArg` generic parameter as well.
Expand Down Expand Up @@ -657,54 +707,6 @@ const updateUser = createAsyncThunk<

While this notation for `state`, `dispatch`, `extra` and `rejectValue` might seem uncommon at first, it allows you to provide only the types for these you actually need - so for example, if you are not accessing `getState` within your `payloadCreator`, there is no need to provide a type for `state`. The same can be said about `rejectValue` - if you don't need to access any potential error payload, you can ignore it.

In addition, you can leverage checks against `action.payload` and `match` as provided by `createAction` as a type-guard for when you want to access known properties on defined types. Example:

- In a reducer

```ts
const usersSlice = createSlice({
name: 'users',
initialState: {
entities: {},
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
state.entities[payload.id] = payload
})
builder.addCase(updateUser.rejected, (state, action) => {
if (action.payload) {
// Since we passed in `MyKnownError` to `rejectValue` in `updateUser`, the type information will be available here.
state.error = action.payload.errorMessage
} else {
state.error = action.error
}
})
},
})
```

- In a component

```ts
const handleUpdateUser = async (userData) => {
const resultAction = await dispatch(updateUser(userData))
if (updateUser.fulfilled.match(resultAction)) {
const user = resultAction.payload
showToast('success', `Updated ${user.name}`)
} else {
if (resultAction.payload) {
// Since we passed in `MyKnownError` to `rejectValue` in `updateUser`, the type information will be available here.
// Note: this would also be a good place to do any handling that relies on the `rejectedWithValue` payload, such as setting field errors
showToast('error', `Update failed: ${resultAction.payload.errorMessage}`)
} else {
showToast('error', `Update failed: ${resultAction.error.message}`)
}
}
}
```

### Defining a Pre-Typed `createAsyncThunk`

As of RTK 1.9, you can define a "pre-typed" version of `createAsyncThunk` that can have the types for `state`, `dispatch`, and `extra` built in. This lets you set up those types once, so you don't have to repeat them each time you call `createAsyncThunk`.
Expand Down

0 comments on commit cba147f

Please sign in to comment.