Skip to content

Commit 12351d7

Browse files
committed
Add createAsyncThunk
1 parent 00a8685 commit 12351d7

File tree

2 files changed

+169
-0
lines changed

2 files changed

+169
-0
lines changed

src/createAsyncThunk.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { createAsyncThunk } from './createAsyncThunk'
2+
3+
describe('createAsyncThunk', () => {
4+
it('creates the action types', () => {
5+
const thunkActionCreator = createAsyncThunk('testType', async () => 42)
6+
7+
expect(thunkActionCreator.fulfilled.type).toBe('testType')
8+
expect(thunkActionCreator.pending.type).toBe('testType/pending')
9+
expect(thunkActionCreator.finished.type).toBe('testType/finished')
10+
expect(thunkActionCreator.rejected.type).toBe('testType/rejected')
11+
})
12+
13+
it('accepts arguments and dispatches the actions on resolve', async () => {
14+
const dispatch = jest.fn()
15+
16+
let passedArgs: any
17+
18+
const result = 42
19+
const args = 123
20+
21+
const thunkActionCreator = createAsyncThunk(
22+
'testType',
23+
async ({ args }) => {
24+
passedArgs = args
25+
return result
26+
}
27+
)
28+
29+
const thunkFunction = thunkActionCreator(args)
30+
31+
await thunkFunction(dispatch, undefined, undefined)
32+
33+
expect(passedArgs).toBe(args)
34+
35+
expect(dispatch).toHaveBeenNthCalledWith(
36+
1,
37+
thunkActionCreator.pending({ args })
38+
)
39+
40+
expect(dispatch).toHaveBeenNthCalledWith(
41+
2,
42+
thunkActionCreator.fulfilled({ args, result })
43+
)
44+
45+
expect(dispatch).toHaveBeenNthCalledWith(
46+
3,
47+
thunkActionCreator.finished({ args })
48+
)
49+
})
50+
51+
it('accepts arguments and dispatches the actions on reject', async () => {
52+
const dispatch = jest.fn()
53+
54+
let passedArgs: any
55+
const args = 123
56+
57+
const error = new Error('Panic!')
58+
59+
const thunkActionCreator = createAsyncThunk('testType', async () => {
60+
throw error
61+
})
62+
63+
const thunkFunction = thunkActionCreator(args)
64+
65+
await thunkFunction(dispatch, undefined, undefined)
66+
67+
expect(dispatch).toHaveBeenNthCalledWith(
68+
1,
69+
thunkActionCreator.pending({ args })
70+
)
71+
72+
expect(dispatch).toHaveBeenNthCalledWith(
73+
2,
74+
thunkActionCreator.rejected({ args, error })
75+
)
76+
77+
expect(dispatch).toHaveBeenNthCalledWith(
78+
3,
79+
thunkActionCreator.finished({ args })
80+
)
81+
})
82+
})

src/createAsyncThunk.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Dispatch } from 'redux'
2+
import { ActionCreatorWithPayload, createAction } from './createAction'
3+
4+
export type Await<T> = T extends {
5+
then(onfulfilled?: (value: infer U) => unknown): unknown
6+
}
7+
? U
8+
: T
9+
10+
export interface AsyncThunkParams<
11+
A,
12+
D extends Dispatch,
13+
S extends unknown,
14+
E extends unknown
15+
> {
16+
args: A
17+
dispatch: D
18+
getState: () => S
19+
extra: E
20+
}
21+
22+
export type AsyncActionCreator<
23+
A,
24+
D extends Dispatch,
25+
S extends unknown,
26+
E extends unknown
27+
> = (params: AsyncThunkParams<A, D, S, E>) => any
28+
29+
export function createAsyncThunk<
30+
ActionType extends string,
31+
PayloadCreator extends AsyncActionCreator<
32+
unknown,
33+
Dispatch,
34+
unknown,
35+
undefined
36+
>
37+
>(type: ActionType, payloadCreator: PayloadCreator) {
38+
type ActionParams = Parameters<PayloadCreator>[0]['args']
39+
40+
const fulfilled = createAction(type) as ActionCreatorWithPayload<
41+
{ args: ActionParams; result: Await<ReturnType<PayloadCreator>> },
42+
ActionType
43+
>
44+
45+
const pending = createAction(type + '/pending') as ActionCreatorWithPayload<
46+
{ args: ActionParams },
47+
string
48+
>
49+
50+
const finished = createAction(type + '/finished') as ActionCreatorWithPayload<
51+
{ args: ActionParams },
52+
string
53+
>
54+
55+
const rejected = createAction(type + '/rejected') as ActionCreatorWithPayload<
56+
{ args: ActionParams; error: Error },
57+
string
58+
>
59+
60+
function actionCreator(args?: ActionParams) {
61+
return async (dispatch: any, getState: any, extra: any) => {
62+
try {
63+
dispatch(pending({ args }))
64+
const result: Await<ReturnType<PayloadCreator>> = await payloadCreator({
65+
args,
66+
dispatch,
67+
getState,
68+
extra
69+
})
70+
// TODO How do we avoid errors in here from hitting the catch clause?
71+
return dispatch(fulfilled({ args, result }))
72+
} catch (err) {
73+
// TODO Errors aren't serializable
74+
dispatch(rejected({ args, error: err }))
75+
} finally {
76+
dispatch(finished({ args }))
77+
}
78+
}
79+
}
80+
81+
actionCreator.pending = pending
82+
actionCreator.rejected = rejected
83+
actionCreator.fulfilled = fulfilled
84+
actionCreator.finished = finished
85+
86+
return actionCreator
87+
}

0 commit comments

Comments
 (0)