Skip to content

Commit ab72f86

Browse files
committed
Implement 'status' prop and 'isPending', 'isFulfilled' and 'isRejected' booleans.
1 parent 3d450b4 commit ab72f86

File tree

8 files changed

+89
-23
lines changed

8 files changed

+89
-23
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,10 @@ is set to `"application/json"`.
382382
- `data` Last resolved promise value, maintained when new error arrives.
383383
- `error` Rejected promise reason, cleared when new data arrives.
384384
- `initialValue` The data or error that was provided through the `initialValue` prop.
385-
- `isLoading` Whether or not a Promise is currently pending.
385+
- `isPending` true when no promise has ever started, or one started but was cancelled.
386+
- `isLoading` true when a promise is currently awaiting settlement.
387+
- `isResolved` true when the last promise was fulfilled with a value.
388+
- `isRejected` true when the last promise was rejected with a reason.
386389
- `startedAt` When the current/last promise was started.
387390
- `finishedAt` When the last promise was resolved or rejected.
388391
- `counter` The number of times a promise was started.

src/Async.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export const createInstance = (defaultProps = {}, displayName = "Async") => {
8383
this.abortController = new window.AbortController()
8484
}
8585
this.counter++
86-
this.dispatch({ type: actionTypes.start, meta: { counter: this.counter } })
86+
this.mounted && this.dispatch({ type: actionTypes.start, meta: { counter: this.counter } })
8787
}
8888

8989
load() {
@@ -118,7 +118,7 @@ export const createInstance = (defaultProps = {}, displayName = "Async") => {
118118
cancel() {
119119
this.counter++
120120
this.abortController.abort()
121-
this.dispatch({ type: actionTypes.cancel, meta: { counter: this.counter } })
121+
this.mounted && this.dispatch({ type: actionTypes.cancel, meta: { counter: this.counter } })
122122
}
123123

124124
onResolve(counter) {

src/index.d.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@ export interface AsyncProps<T> extends AsyncOptions<T> {
2020
children?: AsyncChildren<T>
2121
}
2222

23-
export interface AsyncState<T> {
23+
interface AbstractState<T> {
2424
data?: T
2525
error?: Error
2626
initialValue?: T
27-
isLoading: boolean
2827
startedAt?: Date
2928
finishedAt?: Date
29+
isPending: boolean
30+
isLoading: boolean
31+
isFulfilled: boolean
32+
isRejected: boolean
3033
counter: number
3134
cancel: () => void
3235
run: (...args: any[]) => Promise<T>
@@ -35,6 +38,12 @@ export interface AsyncState<T> {
3538
setError: (error: Error, callback?: () => void) => Error
3639
}
3740

41+
export type AsyncPending<T> = AbstractState<T> & { status: "pending" }
42+
export type AsyncLoading<T> = AbstractState<T> & { status: "loading" }
43+
export type AsyncFulfilled<T> = AbstractState<T> & { status: "fulfilled"; data: T }
44+
export type AsyncRejected<T> = AbstractState<T> & { status: "rejected"; error: Error }
45+
export type AsyncState<T> = AsyncPending<T> | AsyncLoading<T> | AsyncFulfilled<T> | AsyncRejected<T>
46+
3847
declare class Async<T> extends React.Component<AsyncProps<T>, AsyncState<T>> {}
3948

4049
declare namespace Async {

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ import Async from "./Async"
22
export { createInstance } from "./Async"
33
export { default as useAsync, useFetch } from "./useAsync"
44
export default Async
5+
export { statusTypes } from "./status"

src/reducer.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getInitialStatus, getIdleStatus, getStatusProps, statusTypes } from "./status"
2+
13
export const actionTypes = {
24
start: "start",
35
cancel: "cancel",
@@ -9,9 +11,9 @@ export const init = ({ initialValue, promise, promiseFn }) => ({
911
initialValue,
1012
data: initialValue instanceof Error ? undefined : initialValue,
1113
error: initialValue instanceof Error ? initialValue : undefined,
12-
isLoading: !!promise || (promiseFn && !initialValue),
1314
startedAt: promise || promiseFn ? new Date() : undefined,
1415
finishedAt: initialValue ? new Date() : undefined,
16+
...getStatusProps(getInitialStatus(initialValue, promise || promiseFn)),
1517
counter: 0,
1618
})
1719

@@ -20,32 +22,32 @@ export const reducer = (state, { type, payload, meta }) => {
2022
case actionTypes.start:
2123
return {
2224
...state,
23-
isLoading: true,
2425
startedAt: new Date(),
2526
finishedAt: undefined,
27+
...getStatusProps(statusTypes.loading),
2628
counter: meta.counter,
2729
}
2830
case actionTypes.cancel:
2931
return {
3032
...state,
31-
isLoading: false,
3233
startedAt: undefined,
34+
...getStatusProps(getIdleStatus(state.error || state.data)),
3335
counter: meta.counter,
3436
}
3537
case actionTypes.fulfill:
3638
return {
3739
...state,
3840
data: payload,
3941
error: undefined,
40-
isLoading: false,
4142
finishedAt: new Date(),
43+
...getStatusProps(statusTypes.fulfilled),
4244
}
4345
case actionTypes.reject:
4446
return {
4547
...state,
4648
error: payload,
47-
isLoading: false,
4849
finishedAt: new Date(),
50+
...getStatusProps(statusTypes.rejected),
4951
}
5052
}
5153
}

src/status.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export const statusTypes = {
2+
pending: "pending",
3+
loading: "loading",
4+
fulfilled: "fulfilled",
5+
rejected: "rejected",
6+
}
7+
8+
export const getInitialStatus = (value, promise) => {
9+
if (value instanceof Error) return statusTypes.rejected
10+
if (value !== undefined) return statusTypes.fulfilled
11+
if (promise) return statusTypes.loading
12+
return statusTypes.pending
13+
}
14+
15+
export const getIdleStatus = value => {
16+
if (value instanceof Error) return statusTypes.rejected
17+
if (value !== undefined) return statusTypes.fulfilled
18+
return statusTypes.pending
19+
}
20+
21+
export const getStatusProps = status => ({
22+
status,
23+
isPending: status === statusTypes.pending,
24+
isLoading: status === statusTypes.loading,
25+
isFulfilled: status === statusTypes.fulfilled,
26+
isRejected: status === statusTypes.rejected,
27+
})

src/status.spec.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* eslint-disable react/prop-types */
2+
3+
import "jest-dom/extend-expect"
4+
5+
import { getInitialStatus, getIdleStatus, statusTypes } from "./status"
6+
7+
describe("getInitialStatus", () => {
8+
test("returns 'pending' when given an undefined value", () => {
9+
expect(getInitialStatus(undefined)).toEqual(statusTypes.pending)
10+
})
11+
test("returns 'loading' when given only a promise", () => {
12+
expect(getInitialStatus(undefined, Promise.resolve("foo"))).toEqual(statusTypes.loading)
13+
})
14+
test("returns 'rejected' when given an Error value", () => {
15+
expect(getInitialStatus(new Error("oops"))).toEqual(statusTypes.rejected)
16+
})
17+
test("returns 'fulfilled' when given any other value", () => {
18+
expect(getInitialStatus(null)).toEqual(statusTypes.fulfilled)
19+
})
20+
})
21+
22+
describe("getIdleStatus", () => {
23+
test("returns 'pending' when given an undefined value", () => {
24+
expect(getIdleStatus(undefined)).toEqual(statusTypes.pending)
25+
})
26+
test("returns 'rejected' when given an Error value", () => {
27+
expect(getIdleStatus(new Error("oops"))).toEqual(statusTypes.rejected)
28+
})
29+
test("returns 'fulfilled' when given any other value", () => {
30+
expect(getIdleStatus(null)).toEqual(statusTypes.fulfilled)
31+
})
32+
})

src/useAsync.js

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const useAsync = (arg1, arg2) => {
4242
abortController.current = new window.AbortController()
4343
}
4444
counter.current++
45-
dispatch({ type: actionTypes.start, meta: { counter: counter.current } })
45+
isMounted.current && dispatch({ type: actionTypes.start, meta: { counter: counter.current } })
4646
}
4747

4848
const load = () => {
@@ -75,7 +75,7 @@ const useAsync = (arg1, arg2) => {
7575
const cancel = () => {
7676
counter.current++
7777
abortController.current.abort()
78-
dispatch({ type: actionTypes.cancel, meta: { counter: counter.current } })
78+
isMounted.current && dispatch({ type: actionTypes.cancel, meta: { counter: counter.current } })
7979
}
8080

8181
useEffect(() => {
@@ -91,18 +91,14 @@ const useAsync = (arg1, arg2) => {
9191
useEffect(() => () => abortController.current.abort(), [])
9292
useEffect(() => (prevOptions.current = options) && undefined)
9393

94-
useDebugValue(state, ({ startedAt, finishedAt, error }) => {
95-
if (startedAt && (!finishedAt || finishedAt < startedAt)) return `[${counter.current}] Loading`
96-
if (finishedAt) return error ? `[${counter.current}] Rejected` : `[${counter.current}] Resolved`
97-
return `[${counter.current}] Pending`
98-
})
94+
useDebugValue(state, ({ status }) => `[${counter.current}] ${status}`)
9995

10096
return useMemo(
10197
() => ({
10298
...state,
99+
cancel,
103100
run,
104101
reload: () => (lastArgs.current ? run(...lastArgs.current) : load()),
105-
cancel,
106102
setData,
107103
setError,
108104
}),
@@ -131,11 +127,7 @@ const useAsyncFetch = (input, init, { defer, json, ...options } = {}) => {
131127
[JSON.stringify(input), JSON.stringify(init)]
132128
),
133129
})
134-
useDebugValue(state, ({ startedAt, finishedAt, error, counter }) => {
135-
if (startedAt && (!finishedAt || finishedAt < startedAt)) return `[${counter}] Loading`
136-
if (finishedAt) return error ? `[${counter}] Rejected` : `[${counter}] Resolved`
137-
return `[${counter}] Pending`
138-
})
130+
useDebugValue(state, ({ counter, status }) => `[${counter}] ${status}`)
139131
return state
140132
}
141133

0 commit comments

Comments
 (0)