Skip to content

Commit 9dec058

Browse files
author
ben.durrant
committed
Add settled matcher for createAsyncThunk
1 parent 3c0546a commit 9dec058

File tree

4 files changed

+101
-8
lines changed

4 files changed

+101
-8
lines changed

packages/toolkit/src/createAsyncThunk.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@ import type {
55
} from './createAction'
66
import { createAction } from './createAction'
77
import type { ThunkDispatch } from 'redux-thunk'
8-
import type { FallbackIfUnknown, Id, IsAny, IsUnknown } from './tsHelpers'
8+
import type {
9+
ActionFromMatcher,
10+
FallbackIfUnknown,
11+
Id,
12+
IsAny,
13+
IsUnknown,
14+
TypeGuard,
15+
} from './tsHelpers'
916
import { nanoid } from './nanoid'
17+
import { isAnyOf } from './matchers'
1018

1119
// @ts-ignore we need the import of these types due to a bundling issue.
1220
type _Keep = PayloadAction | ActionCreatorWithPreparedPayload<any, unknown>
@@ -415,6 +423,13 @@ export type AsyncThunk<
415423
ThunkArg,
416424
ThunkApiConfig
417425
>
426+
// matchSettled?
427+
settled: (
428+
action: any
429+
) => action is ReturnType<
430+
| AsyncThunkRejectedActionCreator<ThunkArg, ThunkApiConfig>
431+
| AsyncThunkFulfilledActionCreator<Returned, ThunkArg, ThunkApiConfig>
432+
>
418433
typePrefix: string
419434
}
420435

@@ -676,6 +691,7 @@ export const createAsyncThunk = (() => {
676691
pending,
677692
rejected,
678693
fulfilled,
694+
settled: isAnyOf(rejected, fulfilled),
679695
typePrefix,
680696
}
681697
)

packages/toolkit/src/createSlice.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ import type {
77
_ActionCreatorWithPreparedPayload,
88
} from './createAction'
99
import { createAction } from './createAction'
10-
import type { CaseReducer, ReducerWithInitialState } from './createReducer'
10+
import type {
11+
ActionMatcherDescriptionCollection,
12+
CaseReducer,
13+
ReducerWithInitialState,
14+
} from './createReducer'
1115
import { createReducer } from './createReducer'
1216
import type { ActionReducerMapBuilder } from './mapBuilders'
1317
import { executeReducerBuilderCallback } from './mapBuilders'
14-
import type { Id, Tail } from './tsHelpers'
18+
import type { ActionFromMatcher, Id, Matcher, Tail } from './tsHelpers'
1519
import type { InjectConfig } from './combineSlices'
1620
import type {
1721
AsyncThunk,
@@ -283,6 +287,12 @@ export interface AsyncThunkSliceReducerConfig<
283287
State,
284288
ReturnType<AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['fulfilled']>
285289
>
290+
settled?: CaseReducer<
291+
State,
292+
ReturnType<
293+
AsyncThunk<Returned, ThunkArg, ThunkApiConfig>['rejected' | 'fulfilled']
294+
>
295+
>
286296
options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig>
287297
}
288298

@@ -483,7 +493,12 @@ type ActionCreatorForCaseReducer<CR, Type extends string> = CR extends (
483493
type SliceDefinedCaseReducers<CaseReducers extends SliceCaseReducers<any>> = {
484494
[Type in keyof CaseReducers]: CaseReducers[Type] extends infer Definition
485495
? Definition extends AsyncThunkSliceReducerDefinition<any, any, any, any>
486-
? Id<Pick<Required<Definition>, 'fulfilled' | 'rejected' | 'pending'>>
496+
? Id<
497+
Pick<
498+
Required<Definition>,
499+
'fulfilled' | 'rejected' | 'pending' | 'settled'
500+
>
501+
>
487502
: Definition extends {
488503
reducer: infer Reducer
489504
}
@@ -582,6 +597,7 @@ export function createSlice<
582597
sliceCaseReducersByName: {},
583598
sliceCaseReducersByType: {},
584599
actionCreators: {},
600+
sliceMatchers: [],
585601
}
586602

587603
reducerNames.forEach((reducerName) => {
@@ -632,6 +648,9 @@ export function createSlice<
632648
for (let key in finalCaseReducers) {
633649
builder.addCase(key, finalCaseReducers[key] as CaseReducer<any>)
634650
}
651+
for (let sM of context.sliceMatchers) {
652+
builder.addMatcher(sM.matcher, sM.reducer)
653+
}
635654
for (let m of actionMatchers) {
636655
builder.addMatcher(m.matcher, m.reducer)
637656
}
@@ -728,10 +747,11 @@ interface ReducerHandlingContext<State> {
728747
| CaseReducer<State, any>
729748
| Pick<
730749
AsyncThunkSliceReducerDefinition<State, any, any, any>,
731-
'fulfilled' | 'rejected' | 'pending'
750+
'fulfilled' | 'rejected' | 'pending' | 'settled'
732751
>
733752
>
734753
sliceCaseReducersByType: Record<string, CaseReducer<State, any>>
754+
sliceMatchers: ActionMatcherDescriptionCollection<State>
735755
actionCreators: Record<string, Function>
736756
}
737757

@@ -828,7 +848,7 @@ function handleThunkCaseReducerDefinition<State>(
828848
reducerDefinition: AsyncThunkSliceReducerDefinition<State, any, any, any>,
829849
context: ReducerHandlingContext<State>
830850
) {
831-
const { payloadCreator, fulfilled, pending, rejected, options } =
851+
const { payloadCreator, fulfilled, pending, rejected, settled, options } =
832852
reducerDefinition
833853
const thunk = createAsyncThunk(type, payloadCreator, options as any)
834854
context.actionCreators[reducerName] = thunk
@@ -842,11 +862,15 @@ function handleThunkCaseReducerDefinition<State>(
842862
if (rejected) {
843863
context.sliceCaseReducersByType[thunk.rejected.type] = rejected
844864
}
865+
if (settled) {
866+
context.sliceMatchers.push({ matcher: thunk.settled, reducer: settled })
867+
}
845868

846869
context.sliceCaseReducersByName[reducerName] = {
847870
fulfilled: fulfilled || noop,
848871
pending: pending || noop,
849872
rejected: rejected || noop,
873+
settled: settled || noop,
850874
}
851875
}
852876

packages/toolkit/src/tests/createAsyncThunk.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ describe('createAsyncThunk', () => {
3737
expect(thunkActionCreator.typePrefix).toBe('testType')
3838
})
3939

40+
it('includes a settled matcher', () => {
41+
const thunkActionCreator = createAsyncThunk('testType', async () => 42)
42+
expect(thunkActionCreator.settled).toEqual(expect.any(Function))
43+
expect(thunkActionCreator.settled(thunkActionCreator.pending(''))).toBe(
44+
false
45+
)
46+
expect(
47+
thunkActionCreator.settled(thunkActionCreator.rejected(null, ''))
48+
).toBe(true)
49+
expect(
50+
thunkActionCreator.settled(thunkActionCreator.fulfilled(42, ''))
51+
).toBe(true)
52+
})
53+
4054
it('works without passing arguments to the payload creator', async () => {
4155
const thunkActionCreator = createAsyncThunk('testType', async () => 42)
4256

@@ -513,7 +527,7 @@ describe('createAsyncThunk with abortController', () => {
513527
}
514528
)
515529

516-
expect(longRunningAsyncThunk()).toThrow("AbortController is not defined")
530+
expect(longRunningAsyncThunk()).toThrow('AbortController is not defined')
517531
})
518532
})
519533
})
@@ -975,6 +989,7 @@ describe('meta', () => {
975989
expect(thunk.fulfilled).toEqual(expectFunction)
976990
expect(thunk.pending).toEqual(expectFunction)
977991
expect(thunk.rejected).toEqual(expectFunction)
992+
expect(thunk.settled).toEqual(expectFunction)
978993
expect(thunk.fulfilled.type).toBe('a/fulfilled')
979994
})
980995
})

packages/toolkit/src/tests/createSlice.typetest.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {
1616
ThunkDispatch,
1717
ValidateSliceCaseReducers,
1818
} from '@reduxjs/toolkit'
19-
import { configureStore } from '@reduxjs/toolkit'
19+
import { configureStore, isRejected } from '@reduxjs/toolkit'
2020
import { createAction, createSlice } from '@reduxjs/toolkit'
2121
import { expectExactType, expectType, expectUnknown } from './helpers'
2222
import { castDraft } from 'immer'
@@ -642,6 +642,18 @@ const value = actionCreators.anyKey
642642
expectType<TestArg>(action.meta.arg)
643643
expectType<SerializedError>(action.error)
644644
},
645+
settled(state, action) {
646+
expectType<TestState>(state)
647+
if (isRejected(action)) {
648+
expectType<TestState>(state)
649+
expectType<TestArg>(action.meta.arg)
650+
expectType<SerializedError>(action.error)
651+
} else {
652+
expectType<TestState>(state)
653+
expectType<TestArg>(action.meta.arg)
654+
expectType<TestReturned>(action.payload)
655+
}
656+
},
645657
}
646658
),
647659
testExplicitType: create.asyncThunk<
@@ -679,6 +691,19 @@ const value = actionCreators.anyKey
679691
expectType<SerializedError>(action.error)
680692
expectType<TestReject | undefined>(action.payload)
681693
},
694+
settled(state, action) {
695+
expectType<TestState>(state)
696+
if (isRejected(action)) {
697+
expectType<TestState>(state)
698+
expectType<TestArg>(action.meta.arg)
699+
expectType<SerializedError>(action.error)
700+
expectType<TestReject | undefined>(action.payload)
701+
} else {
702+
expectType<TestState>(state)
703+
expectType<TestArg>(action.meta.arg)
704+
expectType<TestReturned>(action.payload)
705+
}
706+
},
682707
}
683708
),
684709
testPretyped: pretypedAsyncThunk(
@@ -702,6 +727,19 @@ const value = actionCreators.anyKey
702727
expectType<SerializedError>(action.error)
703728
expectType<TestReject | undefined>(action.payload)
704729
},
730+
settled(state, action) {
731+
expectType<TestState>(state)
732+
if (isRejected(action)) {
733+
expectType<TestState>(state)
734+
expectType<TestArg>(action.meta.arg)
735+
expectType<SerializedError>(action.error)
736+
expectType<TestReject | undefined>(action.payload)
737+
} else {
738+
expectType<TestState>(state)
739+
expectType<TestArg>(action.meta.arg)
740+
expectType<TestReturned>(action.payload)
741+
}
742+
},
705743
}
706744
),
707745
}

0 commit comments

Comments
 (0)