Skip to content

Commit 5845499

Browse files
authored
Merge pull request #3388 from EskiMojo14/slice-thunks
2 parents ab58782 + 525807a commit 5845499

File tree

8 files changed

+1003
-67
lines changed

8 files changed

+1003
-67
lines changed

docs/api/createSlice.mdx

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,192 @@ const todosSlice = createSlice({
130130
})
131131
```
132132

133+
### The `reducers` "creator callback" notation
134+
135+
Alternatively, the `reducers` field can be a callback which receives a "create" object.
136+
137+
The main benefit of this is that you can create [async thunks](./createAsyncThunk) as part of your slice. Types are also slightly simplified for prepared reducers.
138+
139+
```ts title="Creator callback for reducers"
140+
import { createSlice, nanoid } from '@reduxjs/toolkit'
141+
import type { PayloadAction } from '@reduxjs/toolkit'
142+
143+
interface Item {
144+
id: string
145+
text: string
146+
}
147+
148+
interface TodoState {
149+
loading: boolean
150+
todos: Item[]
151+
}
152+
153+
const todosSlice = createSlice({
154+
name: 'todos',
155+
initialState: {
156+
loading: false,
157+
todos: [],
158+
} as TodoState,
159+
reducers: (create) => ({
160+
deleteTodo: create.reducer((state, action: PayloadAction<number>) => {
161+
state.todos.splice(action.payload, 1)
162+
}),
163+
addTodo: create.preparedReducer(
164+
(text: string) => {
165+
const id = nanoid()
166+
return { payload: { id, text } }
167+
},
168+
// action type is inferred from prepare callback
169+
(state, action) => {
170+
state.todos.push(action.payload)
171+
}
172+
),
173+
fetchTodo: create.asyncThunk(
174+
async (id: string, thunkApi) => {
175+
const res = await fetch(`myApi/todos?id=${id}`)
176+
return (await res.json()) as Item
177+
},
178+
{
179+
pending: (state) => {
180+
state.loading = true
181+
},
182+
rejected: (state, action) => {
183+
state.loading = false
184+
},
185+
fulfilled: (state, action) => {
186+
state.loading = false
187+
state.todos.push(action.payload)
188+
},
189+
}
190+
),
191+
}),
192+
})
193+
194+
export const { addTodo, deleteTodo, fetchTodo } = todosSlice.actions
195+
```
196+
197+
#### Create Methods
198+
199+
#### `create.reducer`
200+
201+
A standard slice case reducer.
202+
203+
**Parameters**
204+
205+
- **reducer** The slice case reducer to use.
206+
207+
```ts no-transpile
208+
create.reducer((state, action: PayloadAction<Todo>) => {
209+
state.todos.push(action.payload)
210+
})
211+
```
212+
213+
#### `create.preparedReducer`
214+
215+
A [prepared](#customizing-generated-action-creators) reducer, to customize the action creator.
216+
217+
**Parameters**
218+
219+
- **prepareAction** The [`prepare callback`](./createAction#using-prepare-callbacks-to-customize-action-contents).
220+
- **reducer** The slice case reducer to use.
221+
222+
The action passed to the case reducer will be inferred from the prepare callback's return.
223+
224+
```ts no-transpile
225+
create.preparedReducer(
226+
(text: string) => {
227+
const id = nanoid()
228+
return { payload: { id, text } }
229+
},
230+
(state, action) => {
231+
state.todos.push(action.payload)
232+
}
233+
)
234+
```
235+
236+
#### `create.asyncThunk`
237+
238+
Creates an async thunk instead of an action creator.
239+
240+
**Parameters**
241+
242+
- **payloadCreator** The thunk [payload creator](./createAsyncThunk#payloadcreator).
243+
- **config** The configuration object. (optional)
244+
245+
The configuration object can contain case reducers for each of the [lifecycle actions](./createAsyncThunk#promise-lifecycle-actions) (`pending`, `fulfilled`, and `rejected`).
246+
247+
Each case reducer will be attached to the slice's `caseReducers` object, e.g. `slice.caseReducers.fetchTodo.fulfilled`.
248+
249+
The configuration object can also contain [`options`](./createAsyncThunk#options).
250+
251+
```ts no-transpile
252+
create.asyncThunk(
253+
async (id: string, thunkApi) => {
254+
const res = await fetch(`myApi/todos?id=${id}`)
255+
return (await res.json()) as Item
256+
},
257+
{
258+
pending: (state) => {
259+
state.loading = true
260+
},
261+
rejected: (state, action) => {
262+
state.loading = false
263+
},
264+
fulfilled: (state, action) => {
265+
state.loading = false
266+
state.todos.push(action.payload)
267+
},
268+
options: {
269+
idGenerator: uuid,
270+
},
271+
}
272+
)
273+
```
274+
275+
:::note
276+
277+
Typing for the `create.asyncThunk` works in the same way as [`createAsyncThunk`](usage/usage-with-typescript#createasyncthunk), with one key difference.
278+
279+
A type for `state` and/or `dispatch` _cannot_ be provided as part of the `ThunkApiConfig`, as this would cause circular types.
280+
281+
Instead, it is necessary to assert the type when needed.
282+
283+
```ts no-transpile
284+
create.asyncThunk<Todo, string, { rejectValue: { error: string } }>(
285+
async (id, thunkApi) => {
286+
const state = thunkApi.getState() as RootState
287+
const dispatch = thunkApi.dispatch as AppDispatch
288+
throw thunkApi.rejectWithValue({
289+
error: 'Oh no!',
290+
})
291+
}
292+
)
293+
```
294+
295+
For common thunk API configuration options, a [`withTypes` helper](usage/usage-with-typescript#defining-a-pre-typed-createasyncthunk) is provided:
296+
297+
```ts no-transpile
298+
reducers: (create) => {
299+
const createAThunk =
300+
create.asyncThunk.withTypes<{ rejectValue: { error: string } }>()
301+
302+
return {
303+
fetchTodo: createAThunk<Todo, string>(async (id, thunkApi) => {
304+
throw thunkApi.rejectWithValue({
305+
error: 'Oh no!',
306+
})
307+
}),
308+
fetchTodos: createAThunk<Todo[], string>(async (id, thunkApi) => {
309+
throw thunkApi.rejectWithValue({
310+
error: 'Oh no, not again!',
311+
})
312+
}),
313+
}
314+
}
315+
```
316+
317+
:::
318+
133319
### `extraReducers`
134320

135321
One of the key concepts of Redux is that each slice reducer "owns" its slice of state, and that many slice reducers

docs/api/getDefaultMiddleware.mdx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,7 @@ to the store. `configureStore` will not add any extra middleware beyond what you
4040
`getDefaultMiddleware` is useful if you want to add some custom middleware, but also still want to have the default
4141
middleware added as well:
4242

43-
```ts
44-
// file: reducer.ts noEmit
45-
46-
export default function rootReducer(state = {}, action: any) {
47-
return state
48-
}
49-
50-
// file: store.ts
43+
```ts no-transpile
5144
import { configureStore } from '@reduxjs/toolkit'
5245

5346
import logger from 'redux-logger'

docs/rtk-query/usage/error-handling.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Redux Toolkit has [action matching utilities](../../api/matching-utilities.mdx#m
8080

8181
:::
8282

83-
```ts title="Error catching middleware example"
83+
```ts no-transpile title="Error catching middleware example"
8484
import { isRejectedWithValue } from '@reduxjs/toolkit'
8585
import type { MiddlewareAPI, Middleware } from '@reduxjs/toolkit'
8686
import { toast } from 'your-cool-library'

packages/toolkit/src/createAsyncThunk.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export const miniSerializeError = (value: any): SerializedError => {
105105
return { message: String(value) }
106106
}
107107

108-
type AsyncThunkConfig = {
108+
export type AsyncThunkConfig = {
109109
state?: unknown
110110
dispatch?: Dispatch
111111
extra?: unknown
@@ -414,7 +414,7 @@ export type AsyncThunk<
414414
typePrefix: string
415415
}
416416

417-
type OverrideThunkApiConfigs<OldConfig, NewConfig> = Id<
417+
export type OverrideThunkApiConfigs<OldConfig, NewConfig> = Id<
418418
NewConfig & Omit<OldConfig, keyof NewConfig>
419419
>
420420

0 commit comments

Comments
 (0)