Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions src/async-data/AsyncView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,25 @@ import React, { ReactElement, ReactNode } from 'react'
import { isFunction } from '../util'

type LoadingFunction = () => ReactNode
type SuccessFunction<Data> = (data: NonNullable<Data>) => ReactNode
type SuccessFunction<Data> = (data: Data) => ReactNode
type ErrorFunction<Error> = (error: NonNullable<Error>) => ReactNode

type Props<Data, Error> = {
data?: Data
error?: Error
isLoading: boolean
renderLoading?: ReactNode | LoadingFunction
renderSuccess: ReactNode | SuccessFunction<Data>
renderError?: ReactNode | ErrorFunction<Error>
}
} & (
| {
allowMissingData: true
renderSuccess: ReactNode | SuccessFunction<Data>
}
| {
allowMissingData?: false
renderSuccess: ReactNode | SuccessFunction<NonNullable<Data>>
}
)

const AsyncView = <Data, Error>(
props: Props<Data, Error>,
Expand All @@ -20,19 +29,34 @@ const AsyncView = <Data, Error>(
const {
data,
error,
isLoading,
renderLoading = null,
renderSuccess,
renderError = null,
allowMissingData = false,
} = props

if (isLoading) {
return <>{isFunction(renderLoading) ? renderLoading() : renderLoading}</>
}

if (error !== null && error !== undefined) {
return <>{isFunction(renderError) ? renderError(error) : renderError}</>
} else if (data !== null && data !== undefined) {
return (
<>{isFunction(renderSuccess) ? renderSuccess(data) : renderSuccess}</>
}

if ((data === undefined || data === null) && !allowMissingData) {
throw new Error(
'Data passed into AsyncView was null or undefined. Use allowMissingData=true if this is intended.',
)
} else {
return <>{isFunction(renderLoading) ? renderLoading() : renderLoading}</>
}

return (
<>
{isFunction(renderSuccess)
? renderSuccess(data as NonNullable<Data>)
: renderSuccess}
</>
)
}

export { AsyncView }
56 changes: 48 additions & 8 deletions src/async-data/__test__/AsyncView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AsyncView } from '../AsyncView'

type Data = {
greeting: string
}
} | null

type Error = {
message: string
Expand All @@ -15,36 +15,76 @@ const Loading: React.FC = () => {
}

const Success: React.FC<{ data: Data }> = ({ data }) => {
return <>{data.greeting}</>
return <>{data?.greeting}</>
}

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

function renderAsyncView(data?: Data, error?: Error): RenderResult {
type RenderAsyncViewProps = {
isLoading: boolean
error?: Error
data?: Data
allowMissingData?: boolean
}

function renderAsyncView({
isLoading,
data,
error,
allowMissingData = false,
}: RenderAsyncViewProps): RenderResult {
return render(
<AsyncView<Data, Error>
<AsyncView
isLoading={isLoading}
data={data}
error={error}
allowMissingData={allowMissingData}
renderLoading={<Loading />}
renderSuccess={(data) => <Success data={data} />}
renderSuccess={(data: unknown) => <Success data={data as Data} />}
renderError={(error) => <Error error={error} />}
/>,
)
}

test('should render loading if asyncState is loading for the first time', function () {
const { getByText } = renderAsyncView()
const { getByText } = renderAsyncView({ isLoading: true })
expect(getByText(/loading/i)).toBeInTheDocument()
})

test('should render success if asyncState is successful', function () {
const { getByText } = renderAsyncView({ greeting: 'Hello' })
const { getByText } = renderAsyncView({
isLoading: false,
data: { greeting: 'Hello' },
})
expect(getByText(/hello/i)).toBeInTheDocument()
})

test.each([null, undefined])(
'should throw asyncState is successful but data is missing',
function (value) {
expect(() => renderAsyncView({ isLoading: false, data: value })).toThrow()
},
)

test.each([null, undefined])(
'should render success if asyncState is successful but data is missing but allowed',
function (value) {
expect(() =>
renderAsyncView({
isLoading: false,
data: value,
allowMissingData: true,
}),
).not.toThrow()
},
)

test('should render error if asyncState is error', function () {
const { getByText } = renderAsyncView(undefined, { message: 'Error' })
const { getByText } = renderAsyncView({
isLoading: false,
error: { message: 'Error' },
})
expect(getByText(/error/i)).toBeInTheDocument()
})
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
"moduleResolution": "node",
"skipLibCheck": true
},
"exclude": ["node_modules", "dist", "**/__test__/*", "**/__tests__/*"],
"exclude": ["node_modules", "dist"],
"include": ["**/*.ts", "**/*.tsx", "**/*.js"]
}