Skip to content

Commit ae8ed4f

Browse files
authored
Merge pull request #17 from aboutbits/ab-180-async-view-should-accept-an-isloading-flag
update AsyncView to work with swr's loading flag and support nullable…
2 parents 4bb6ae7 + 507f4a1 commit ae8ed4f

3 files changed

Lines changed: 81 additions & 17 deletions

File tree

src/async-data/AsyncView.tsx

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,25 @@ import React, { ReactElement, ReactNode } from 'react'
22
import { isFunction } from '../util'
33

44
type LoadingFunction = () => ReactNode
5-
type SuccessFunction<Data> = (data: NonNullable<Data>) => ReactNode
5+
type SuccessFunction<Data> = (data: Data) => ReactNode
66
type ErrorFunction<Error> = (error: NonNullable<Error>) => ReactNode
77

88
type Props<Data, Error> = {
99
data?: Data
1010
error?: Error
11+
isLoading: boolean
1112
renderLoading?: ReactNode | LoadingFunction
12-
renderSuccess: ReactNode | SuccessFunction<Data>
1313
renderError?: ReactNode | ErrorFunction<Error>
14-
}
14+
} & (
15+
| {
16+
allowMissingData: true
17+
renderSuccess: ReactNode | SuccessFunction<Data>
18+
}
19+
| {
20+
allowMissingData?: false
21+
renderSuccess: ReactNode | SuccessFunction<NonNullable<Data>>
22+
}
23+
)
1524

1625
const AsyncView = <Data, Error>(
1726
props: Props<Data, Error>,
@@ -20,19 +29,34 @@ const AsyncView = <Data, Error>(
2029
const {
2130
data,
2231
error,
32+
isLoading,
2333
renderLoading = null,
2434
renderSuccess,
2535
renderError = null,
36+
allowMissingData = false,
2637
} = props
38+
39+
if (isLoading) {
40+
return <>{isFunction(renderLoading) ? renderLoading() : renderLoading}</>
41+
}
42+
2743
if (error !== null && error !== undefined) {
2844
return <>{isFunction(renderError) ? renderError(error) : renderError}</>
29-
} else if (data !== null && data !== undefined) {
30-
return (
31-
<>{isFunction(renderSuccess) ? renderSuccess(data) : renderSuccess}</>
45+
}
46+
47+
if ((data === undefined || data === null) && !allowMissingData) {
48+
throw new Error(
49+
'Data passed into AsyncView was null or undefined. Use allowMissingData=true if this is intended.',
3250
)
33-
} else {
34-
return <>{isFunction(renderLoading) ? renderLoading() : renderLoading}</>
3551
}
52+
53+
return (
54+
<>
55+
{isFunction(renderSuccess)
56+
? renderSuccess(data as NonNullable<Data>)
57+
: renderSuccess}
58+
</>
59+
)
3660
}
3761

3862
export { AsyncView }

src/async-data/__test__/AsyncView.test.tsx

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { AsyncView } from '../AsyncView'
44

55
type Data = {
66
greeting: string
7-
}
7+
} | null
88

99
type Error = {
1010
message: string
@@ -15,36 +15,76 @@ const Loading: React.FC = () => {
1515
}
1616

1717
const Success: React.FC<{ data: Data }> = ({ data }) => {
18-
return <>{data.greeting}</>
18+
return <>{data?.greeting}</>
1919
}
2020

2121
const Error: React.FC<{ error: Error }> = ({ error }) => {
2222
return <>{error.message}</>
2323
}
2424

25-
function renderAsyncView(data?: Data, error?: Error): RenderResult {
25+
type RenderAsyncViewProps = {
26+
isLoading: boolean
27+
error?: Error
28+
data?: Data
29+
allowMissingData?: boolean
30+
}
31+
32+
function renderAsyncView({
33+
isLoading,
34+
data,
35+
error,
36+
allowMissingData = false,
37+
}: RenderAsyncViewProps): RenderResult {
2638
return render(
27-
<AsyncView<Data, Error>
39+
<AsyncView
40+
isLoading={isLoading}
2841
data={data}
2942
error={error}
43+
allowMissingData={allowMissingData}
3044
renderLoading={<Loading />}
31-
renderSuccess={(data) => <Success data={data} />}
45+
renderSuccess={(data: unknown) => <Success data={data as Data} />}
3246
renderError={(error) => <Error error={error} />}
3347
/>,
3448
)
3549
}
3650

3751
test('should render loading if asyncState is loading for the first time', function () {
38-
const { getByText } = renderAsyncView()
52+
const { getByText } = renderAsyncView({ isLoading: true })
3953
expect(getByText(/loading/i)).toBeInTheDocument()
4054
})
4155

4256
test('should render success if asyncState is successful', function () {
43-
const { getByText } = renderAsyncView({ greeting: 'Hello' })
57+
const { getByText } = renderAsyncView({
58+
isLoading: false,
59+
data: { greeting: 'Hello' },
60+
})
4461
expect(getByText(/hello/i)).toBeInTheDocument()
4562
})
4663

64+
test.each([null, undefined])(
65+
'should throw asyncState is successful but data is missing',
66+
function (value) {
67+
expect(() => renderAsyncView({ isLoading: false, data: value })).toThrow()
68+
},
69+
)
70+
71+
test.each([null, undefined])(
72+
'should render success if asyncState is successful but data is missing but allowed',
73+
function (value) {
74+
expect(() =>
75+
renderAsyncView({
76+
isLoading: false,
77+
data: value,
78+
allowMissingData: true,
79+
}),
80+
).not.toThrow()
81+
},
82+
)
83+
4784
test('should render error if asyncState is error', function () {
48-
const { getByText } = renderAsyncView(undefined, { message: 'Error' })
85+
const { getByText } = renderAsyncView({
86+
isLoading: false,
87+
error: { message: 'Error' },
88+
})
4989
expect(getByText(/error/i)).toBeInTheDocument()
5090
})

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
"moduleResolution": "node",
55
"skipLibCheck": true
66
},
7-
"exclude": ["node_modules", "dist", "**/__test__/*", "**/__tests__/*"],
7+
"exclude": ["node_modules", "dist"],
88
"include": ["**/*.ts", "**/*.tsx", "**/*.js"]
99
}

0 commit comments

Comments
 (0)