Skip to content

Commit a38b3b7

Browse files
update TS docs, add new 1.3.0 APIs (#388)
* Docs: add info on how to type Meta in `createSlice` * Docs: better example for signal.addEventListener * docs: TS usage for `createAsyncThunk` * docs: TS docs for `createEntityAdapter` * Edit new TS usage docs Co-authored-by: Mark Erikson <[email protected]>
1 parent 663fb05 commit a38b3b7

File tree

3 files changed

+190
-32
lines changed

3 files changed

+190
-32
lines changed

docs/api/createAsyncThunk.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -276,22 +276,24 @@ const readStream = createAsyncThunk('readStream', async (stream: ReadableStream,
276276
#### Listening for Abort Events
277277
278278
You can also call `signal.addEventListener('abort', callback)` to have logic inside the thunk be notified when `promise.abort()` was called.
279+
This can for example be used in conjunction with an axios `CancelToken`:
279280
280281
```ts
281-
const readStream = createAsyncThunk(
282-
'readStream',
283-
(arg, { signal }) =>
284-
new Promise((resolve, reject) => {
285-
signal.addEventListener('abort', () => {
286-
reject(new DOMException('Was aborted while running', 'AbortError'))
287-
})
288-
289-
startActionA(arg)
290-
.then(startActionB)
291-
.then(startActionC)
292-
.then(startActionD)
293-
.then(resolve)
282+
import { createAsyncThunk } from '@reduxjs/toolkit'
283+
import axios from 'axios'
284+
285+
const fetchUserById = createAsyncThunk(
286+
'users/fetchById',
287+
async (userId, { signal }) => {
288+
const source = axios.CancelToken.source()
289+
signal.addEventListener('abort', () => {
290+
source.cancel()
294291
})
292+
const response = await axios.get(`https://reqres.in/api/users/${userId}`, {
293+
cancelToken: source.token
294+
})
295+
return response.data
296+
}
295297
)
296298
```
297299

docs/api/createEntityAdapter.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const booksAdapter = createEntityAdapter({
5252
const booksSlice = createSlice({
5353
name: 'books',
5454
initialState: booksAdapter.getInitialState(),
55-
reducer: {
55+
reducers: {
5656
// Can pass adapter functions directly as case reducers. Because we're passing this
5757
// as a value, `createSlice` will auto-generate the `bookAdded` action type / creator
5858
bookAdded: booksAdapter.addOne,

docs/usage/usage-with-typescript.md

Lines changed: 174 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Using [configureStore](../api/configureStore.md) should not need any additional
2222
The easiest way of getting the `State` type is to define the root reducer in advance and extract its `ReturnType`.
2323
It is recommend to give the type a different name like `RootState` to prevent confusion, as the type name `State` is usually overused.
2424

25-
```typescript
25+
```typescript {3}
2626
import { combineReducers } from '@reduxjs/toolkit'
2727
const rootReducer = combineReducers({})
2828
export type RootState = ReturnType<typeof rootReducer>
@@ -33,7 +33,7 @@ export type RootState = ReturnType<typeof rootReducer>
3333
If you want to get the `Dispatch` type from your store, you can extract it after creating the store.
3434
It is recommend to give the type a different name like `AppDispatch` to prevent confusion, as the type name `Dispatch` is usually overused.
3535
36-
```typescript
36+
```typescript {6}
3737
import { configureStore } from '@reduxjs/toolkit'
3838
import rootReducer from './rootReducer'
3939
const store = configureStore({
@@ -50,7 +50,7 @@ There might however be cases, where TypeScript decides to simplify your provided
5050
5151
Please note that when calling `getDefaultMiddleware` in TypeScript, you have to provide the state type as a generic argument.
5252
53-
```ts
53+
```ts {10-20}
5454
import { configureStore } from '@reduxjs/toolkit'
5555
import additionalMiddleware from 'additional-middleware'
5656
// @ts-ignore
@@ -110,7 +110,7 @@ createAction('test', withPayloadType<string>())
110110

111111
### Alternative to using a literally-typed `action.type`
112112

113-
If you are using `action.type` as discriminator on a discriminated union, for example to correctly type your payload in `case` statements, you might be interested in this alternative:
113+
If you are using `action.type` as a discriminator on a discriminated union, for example to correctly type your payload in `case` statements, you might be interested in this alternative:
114114

115115
Created action creators have a `match` method that acts as a [type predicate](https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates):
116116

@@ -128,7 +128,7 @@ This `match` method is also very useful in combination with `redux-observable` a
128128

129129
## `createReducer`
130130

131-
The default way of calling `createReducer` would be with a map object, like this:
131+
The default way of calling `createReducer` would be with a "lookup table" / "map object", like this:
132132

133133
```typescript
134134
createReducer(0, {
@@ -159,7 +159,7 @@ As an alternative, RTK includes a type-safe reducer builder API.
159159

160160
Instead of using a simple object as an argument to `createReducer`, you can also use a callback that receives a `ActionReducerMapBuilder` instance:
161161

162-
```typescript
162+
```typescript {3-10}
163163
const increment = createAction<number, 'increment'>('increment')
164164
const decrement = createAction<number, 'decrement'>('decrement')
165165
createReducer(0, builder =>
@@ -213,33 +213,68 @@ createSlice({
213213
})
214214
```
215215

216-
### Defining the type of your `initialState`
216+
### Defining the Initial State Type
217217

218218
You might have noticed that it is not a good idea to pass your `SliceState` type as a generic to `createSlice`. This is due to the fact that in almost all cases, follow-up generic parameters to `createSlice` need to be inferred, and TypeScript cannot mix explicit declaration and inference of generic types within the same "generic block".
219219

220-
Instead, you can use the construct `initialState: myInitialState as SliceState`.
220+
The standard approach is to declare an interface or type for your state, create an initial state value that uses that type, and pass the initial state value to `createSlice. You can also use the construct`initialState: myInitialState as SliceState`.
221221

222-
```ts
222+
```ts {1,4,8,15}
223223
type SliceState = { state: 'loading' } | { state: 'finished'; data: string }
224224

225+
// First approach: define the initial state using that type
226+
const initialState: SliceState = { state: 'loading' }
227+
225228
createSlice({
226-
name: 'test',
229+
name: 'test1',
230+
initialState, // type SliceState is inferred for the state of the slice
231+
reducers: {}
232+
})
233+
234+
// Or, cast the initial state as necessary
235+
createSlice({
236+
name: 'test2',
227237
initialState: { state: 'loading' } as SliceState,
228-
reducers: {
229-
// ...
230-
}
238+
reducers: {}
231239
})
232240
```
233241

234242
which will result in a `Slice<SliceState, ...>`.
235243

236-
### On the "type" property of slice action Reducers
244+
### Defining Action Contents with `prepare` Callbacks
237245

238-
As TS cannot combine two string literals (`slice.name` and the key of `actionMap`) into a new literal, all actionCreators created by createSlice are of type 'string'. This is usually not a problem, as these types are only rarely used as literals.
246+
If you want to add a `meta` or `error` property to your action, or customize the `payload` of your action, you have to use the `prepare` notation.
239247

240-
In most cases that type would be required as a literal, the `slice.action.myAction.match` [type predicate](https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates) should prove as a viable alternative:
248+
Using this notation with TypeScript looks like this:
241249

242-
```typescript
250+
```ts {5-16}
251+
const blogSlice = createSlice({
252+
name: 'blogData',
253+
initialState,
254+
reducers: {
255+
receivedAll: {
256+
reducer(
257+
state,
258+
action: PayloadAction<Page[], string, { currentPage: number }>
259+
) {
260+
state.all = action.payload
261+
state.meta = action.meta
262+
},
263+
prepare(payload: Page[], currentPage: number) {
264+
return { payload, meta: { currentPage } }
265+
}
266+
}
267+
}
268+
})
269+
```
270+
271+
### Generated Action Types for Slices
272+
273+
As TS cannot combine two string literals (`slice.name` and the key of `actionMap`) into a new literal, all actionCreators created by `createSlice` are of type 'string'. This is usually not a problem, as these types are only rarely used as literals.
274+
275+
In most cases that `type` would be required as a literal, the `slice.action.myAction.match` [type predicate](https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates) should be a viable alternative:
276+
277+
```ts {10}
243278
const slice = createSlice({
244279
name: 'test',
245280
initialState: 0,
@@ -259,7 +294,44 @@ If you actually _need_ that type, unfortunately there is no other way than manua
259294

260295
### Type safety with `extraReducers`
261296

262-
Like in `createReducer`, the `extraReducers` map object is not easy to fully type. So, like with `createReducer`, you may also use the "builder callback" approach for defining the reducer object argument. See [the `createReducer` section above](#createreducer) for an example.
297+
Reducer lookup tables that map an action `type` string to a reducer function are not easy to fully type correctly. This affects both `createReducer` and the `extraReducers` argument for `createSlice`. So, like with `createReducer`, [you may also use the "builder callback" approach](#building-type-safe-reducer-argument-objects) for defining the reducer object argument.
298+
299+
This is particularly useful when a slice reducer needs to handle action types generated by other slices, or generated by specific calls to `createAction` (such as the actions generated by [`createAsyncThunk`](../api/createAsyncThunk.md)).
300+
301+
```ts {27-30}
302+
const fetchUserById = createAsyncThunk(
303+
'users/fetchById',
304+
// if you type your function argument here
305+
async (userId: number) => {
306+
const response = await fetch(`https://reqres.in/api/users/${userId}`)
307+
return (await response.json()) as Returned
308+
}
309+
)
310+
311+
interface UsersState {
312+
entities: []
313+
loading: 'idle' | 'pending' | 'succeeded' | 'failed'
314+
}
315+
316+
const initialState: UsersState = {
317+
entities: [],
318+
loading: 'idle'
319+
}
320+
321+
const usersSlice = createSlice({
322+
name: 'users',
323+
initialState,
324+
reducers: {
325+
// fill in primary logic here
326+
},
327+
extraReducers: builder => {
328+
builder.addCase(fetchUserById.pending, (state, action) => {
329+
// both `state` and `action` are now correctly typed
330+
// based on the slice state and the `pending` action creator
331+
})
332+
}
333+
})
334+
```
263335

264336
### Wrapping `createSlice`
265337

@@ -319,3 +391,87 @@ const wrappedSlice = createGenericSlice({
319391
}
320392
})
321393
```
394+
395+
## `createAsyncThunk`
396+
397+
In the most common use cases, you should not need to explicitly declare any types for the `createAsyncThunk` call itself.
398+
399+
Just provide a type for the first argument to the `payloadCreator` argument as you would for any function argument, and the resulting thunk will accept the same type as input parameter.
400+
The return type of the `payloadCreator` will also be reflected in all generated action types.
401+
402+
```ts {8,17}
403+
interface Returned {
404+
// ...
405+
}
406+
407+
const fetchUserById = createAsyncThunk(
408+
'users/fetchById',
409+
// if you type your function argument here
410+
async (userId: number) => {
411+
const response = await fetch(`https://reqres.in/api/users/${userId}`)
412+
return (await response.json()) as Returned
413+
}
414+
)
415+
416+
// the parameter of `fetchUserById` is automatically inferred to `number` here
417+
// and dispatching the resulting thunkAction will return a Promise of a correctly
418+
// typed "fulfilled" or "rejected" action.
419+
const lastReturnedAction = await store.dispatch(fetchUserById(3))
420+
```
421+
422+
The second argument to the `payloadCreator` is a `thunkApi` object containing references to the `dispatch`, `getState`, and `extra` arguments from the thunk middleware. 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.
423+
424+
To define the types for these arguments, pass an object as the third generic argument, with type declarations for some or all of these fields: `{dispatch?, state?, extra?}`.
425+
426+
```ts {2-10}
427+
const fetchUserById = createAsyncThunk<
428+
Returned,
429+
number,
430+
{
431+
dispatch: AppDispatch
432+
state: State
433+
extra: {
434+
jwt: string
435+
}
436+
}
437+
>('users/fetchById', async (userId, thunkApi) => {
438+
const response = await fetch(`https://reqres.in/api/users/${userId}`, {
439+
headers: {
440+
Authorization: `Bearer ${thunkApi.extra.jwt}`
441+
}
442+
})
443+
return await response.json()
444+
})
445+
```
446+
447+
While this notation for `state`, `dispatch` and `extra` 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`.
448+
449+
## `createEntityAdapter`
450+
451+
Typing `createEntityAdapter` only requires you to specify the entity type as the single generic argument.
452+
453+
The example from the `createEntityAdapter` documentation would look like this in TypeScript:
454+
455+
```ts {7}
456+
interface Book {
457+
bookId: number
458+
title: string
459+
// ...
460+
}
461+
462+
const booksAdapter = createEntityAdapter<Book>({
463+
selectId: book => book.bookId,
464+
sortComparer: (a, b) => a.title.localeCompare(b.title)
465+
})
466+
467+
const booksSlice = createSlice({
468+
name: 'books',
469+
initialState: booksAdapter.getInitialState(),
470+
reducers: {
471+
bookAdded: booksAdapter.addOne,
472+
booksReceived(state, action: PayloadAction<{ books: Book[] }>) {
473+
booksAdapter.setAll(state, action.payload.books)
474+
}
475+
}
476+
})
477+
```

0 commit comments

Comments
 (0)