Skip to content

Commit c299f05

Browse files
committed
feat: 🎸 add cancel action support
✅ Closes: #3
1 parent 8b90e73 commit c299f05

File tree

5 files changed

+96
-17
lines changed

5 files changed

+96
-17
lines changed

‎README.md

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,23 @@
88

99
# react-redux-dispatch-async
1010

11-
👉 REDUX _middleware_ waiting async _actions_ with **SUFFIXES** 👈
11+
👉 REDUX _middleware_ & **HOOK** 🎉 waiting async _actions_ with **SUFFIXES** 👈
1212

1313
```
14-
+------------------+
15-
+----->+ ACTION_SUCCEEDED +
16-
| +------------------+
17-
+------------------+ |
18-
+ ACTION_REQUESTED +----+
19-
+------------------+ |
14+
15+
+------------------+
16+
| ACTION_REQUESTED |----+
17+
+------------------+ | +------------------+
18+
+----->| ACTION_SUCCEEDED |
2019
| +------------------+
21-
+----->+ ACTION_FAILED +
22-
+------------------+
20+
|
21+
| +--------------------+
22+
+----->| ACTION_FAILED |
23+
| +--------------------+
24+
|
25+
| +--------------------+
26+
+----->| ACTION_CANCELED |
27+
+--------------------+
2328
```
2429

2530
## Install
@@ -47,6 +52,8 @@ export default function MyUserInterface({ id }: { id: string }) {
4752
return <User {...result} />
4853
case 'timeout':
4954
return <Text>{'timeout ¯\\_(ツ)_//¯'}</Text>
55+
case 'canceled':
56+
return <Text>{'canceled ¯\\_(ツ)_//¯'}</Text>
5057
default:
5158
return null
5259
}
@@ -69,6 +76,7 @@ const store = createStore(
6976
request: 'REQUEST', // 👈 define your own async suffixes
7077
success: 'SUCCESS',
7178
failure: 'FAILURE',
79+
cancel: 'CANCEL', // optional
7280
}),
7381
),
7482
)
@@ -79,6 +87,7 @@ const store = createStore(
7987
- `[...]_REQUESTED`
8088
- `[...]_SUCCEEDED`
8189
- `[...]_FAILED`
90+
- `[...]_CANCELED`
8291

8392
## Two functions
8493

@@ -89,6 +98,7 @@ dispatchAsyncMiddleware: (c?: {
8998
request: string
9099
success: string
91100
failure: string
101+
cancel?: string
92102
}) => redux.Middleware
93103
```
94104

‎src/DispatchAsyncMiddleware.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import { Action, Middleware } from 'redux'
22
import { listeners } from './ActionListener'
33

4-
export interface ActionSuffix {
4+
export interface NormalSuffix {
55
request: string
66
success: string
77
failure: string
8-
cancel?: string
98
}
109

10+
export interface CancelableSuffix {
11+
request: string
12+
success: string
13+
failure: string
14+
cancel: string
15+
}
16+
17+
export type ActionSuffix = NormalSuffix | CancelableSuffix
18+
1119
export const ConfigMiddleware: {
1220
suffixes: ActionSuffix
1321
initialized: boolean
@@ -17,14 +25,15 @@ export const ConfigMiddleware: {
1725
request: 'REQUESTED',
1826
success: 'SUCCEEDED',
1927
failure: 'FAILED',
20-
cancel: undefined,
28+
cancel: 'CANCELED',
2129
},
2230
}
2331

2432
const isAsyncAction = (config: ActionSuffix, action: Action) =>
2533
action.type.endsWith(config.request) ||
2634
action.type.endsWith(config.success) ||
27-
action.type.endsWith(config.failure)
35+
action.type.endsWith(config.failure) ||
36+
action.type.endsWith((config as CancelableSuffix).cancel)
2837

2938
export const createDispatchAsyncMiddleware: (
3039
config?: ActionSuffix,

‎src/dispatchAsync.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Action } from 'redux'
22
import { addActionListener } from './ActionListener'
3-
import { ConfigMiddleware } from './DispatchAsyncMiddleware'
3+
import { CancelableSuffix, ConfigMiddleware } from './DispatchAsyncMiddleware'
44

55
interface DispatchAsyncResultSuccess<T = any> {
66
success: true
@@ -10,6 +10,7 @@ interface DispatchAsyncResultSuccess<T = any> {
1010
interface DispatchAsyncResultError {
1111
success: false
1212
error: Error
13+
canceled?: true
1314
}
1415

1516
export type DispatchAsyncResult<T = any> =
@@ -45,6 +46,18 @@ export function dispatchAsync<T = any>(
4546
: new Error(`Action failure: ${actionNameBase}`)
4647
resolve({ success: false, error })
4748
unsubscribe()
49+
} else if (
50+
resultAction.type ===
51+
`${actionNameBase}_${
52+
(ConfigMiddleware.suffixes as CancelableSuffix).cancel
53+
}`
54+
) {
55+
resolve({
56+
success: false,
57+
error: new Error('canceled'),
58+
canceled: true,
59+
})
60+
unsubscribe()
4861
}
4962
})
5063
dispatch(action)

‎src/useDispatchAsync.test.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ const dumbRequest = () => ({
3333
payload: undefined,
3434
})
3535

36+
const cancelableActionRequest = () => ({
37+
type: 'CANCELABLE_ACTION_REQUESTED',
38+
payload: undefined,
39+
})
40+
41+
const cancelableActionCancel = () => ({
42+
type: 'CANCELABLE_ACTION_CANCELED',
43+
payload: undefined,
44+
})
45+
3646
const ReduxProvider = ({
3747
children,
3848
store,
@@ -65,6 +75,10 @@ sagaMiddleware.run(function*() {
6575
yield delay(1000)
6676
yield put(loadUsersFailure(new Error('load user failed')))
6777
}),
78+
takeEvery('CANCELABLE_ACTION_REQUESTED', function*() {
79+
yield delay(1000)
80+
yield put(cancelableActionCancel())
81+
}),
6882
])
6983
})
7084

@@ -111,3 +125,14 @@ test('should return timeout status', async () => {
111125
await wait(() => result.current.status !== 'loading')
112126
expect(result.current.status).toBe('timeout')
113127
})
128+
129+
test('should return cancel status', async () => {
130+
const { result, wait } = renderHook(
131+
() =>
132+
useDispatchAsync<{ id: string; name: string }>(cancelableActionRequest),
133+
{ wrapper },
134+
)
135+
expect(result.current.status).toBe('loading')
136+
await wait(() => result.current.status !== 'loading')
137+
expect(result.current.status).toBe('canceled')
138+
})

‎src/useDispatchAsync.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ interface ResultError {
1717
error: Error
1818
}
1919

20+
interface ResultCancel {
21+
status: 'canceled'
22+
}
23+
2024
interface ResultTimeout {
2125
status: 'timeout'
2226
}
@@ -30,6 +34,7 @@ export type UseDispatchAsync<R = unknown> =
3034
| ResultSuccess<R>
3135
| ResultError
3236
| ResultTimeout
37+
| ResultCancel
3338
| ResultUnknown
3439

3540
export type Status = Pick<UseDispatchAsync, 'status'>['status']
@@ -52,6 +57,7 @@ export function useDispatchAsync<R = any>(
5257
const [result, setResult] = useState<R | undefined>(undefined)
5358
const [error, setError] = useState<Error | undefined>(undefined)
5459
const [isTimeout, setIsTimeout] = useState<boolean>(false)
60+
const [isCancel, setIsCancel] = useState<boolean>(false)
5561

5662
// 👉 race condition to get last update
5763
// https://sebastienlorber.com/handling-api-request-race-conditions-in-react
@@ -73,8 +79,18 @@ export function useDispatchAsync<R = any>(
7379
.then((res) => {
7480
// filtering last update promise
7581
if (currentPromise === lastPromise.current) {
82+
// filtering timeout
7683
if (typeof res !== 'boolean') {
77-
res.success ? setResult(res.result) : setError(res.error)
84+
// filtering success
85+
if (res.success) {
86+
setResult(res.result)
87+
} else {
88+
if (!res.canceled) {
89+
setError(res.error)
90+
} else {
91+
setIsCancel(true)
92+
}
93+
}
7894
} else {
7995
setIsTimeout(true)
8096
}
@@ -89,13 +105,18 @@ export function useDispatchAsync<R = any>(
89105
}, deps)
90106

91107
const status: Status = useMemo(() => {
92-
if (!result && !error && !isTimeout) {
108+
if (!result && !error && !isTimeout && !isCancel) {
93109
return 'loading'
94110
}
111+
95112
if (result) {
96113
return 'success'
97114
}
98115

116+
if (isCancel) {
117+
return 'canceled'
118+
}
119+
99120
if (error) {
100121
return 'error'
101122
}
@@ -105,11 +126,12 @@ export function useDispatchAsync<R = any>(
105126
}
106127

107128
return 'unknown'
108-
}, [result, error, isTimeout])
129+
}, [result, error, isTimeout, isCancel])
109130

110131
switch (status) {
111132
case 'loading':
112133
case 'timeout':
134+
case 'canceled':
113135
return { status }
114136
case 'success':
115137
return { result: result!, status }

0 commit comments

Comments
 (0)