diff --git a/src/async-data/AsyncView.tsx b/src/async-data/AsyncView.tsx index 2465666..0673811 100644 --- a/src/async-data/AsyncView.tsx +++ b/src/async-data/AsyncView.tsx @@ -2,16 +2,25 @@ import React, { ReactElement, ReactNode } from 'react' import { isFunction } from '../util' type LoadingFunction = () => ReactNode -type SuccessFunction = (data: NonNullable) => ReactNode +type SuccessFunction = (data: Data) => ReactNode type ErrorFunction = (error: NonNullable) => ReactNode type Props = { data?: Data error?: Error + isLoading: boolean renderLoading?: ReactNode | LoadingFunction - renderSuccess: ReactNode | SuccessFunction renderError?: ReactNode | ErrorFunction -} +} & ( + | { + allowMissingData: true + renderSuccess: ReactNode | SuccessFunction + } + | { + allowMissingData?: false + renderSuccess: ReactNode | SuccessFunction> + } +) const AsyncView = ( props: Props, @@ -20,19 +29,34 @@ const AsyncView = ( 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) + : renderSuccess} + + ) } export { AsyncView } diff --git a/src/async-data/__test__/AsyncView.test.tsx b/src/async-data/__test__/AsyncView.test.tsx index 6abe4e8..9898692 100644 --- a/src/async-data/__test__/AsyncView.test.tsx +++ b/src/async-data/__test__/AsyncView.test.tsx @@ -4,7 +4,7 @@ import { AsyncView } from '../AsyncView' type Data = { greeting: string -} +} | null type Error = { message: string @@ -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( - + } - renderSuccess={(data) => } + renderSuccess={(data: unknown) => } renderError={(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() }) diff --git a/tsconfig.json b/tsconfig.json index 5a8dc5a..341f0e5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,6 @@ "moduleResolution": "node", "skipLibCheck": true }, - "exclude": ["node_modules", "dist", "**/__test__/*", "**/__tests__/*"], + "exclude": ["node_modules", "dist"], "include": ["**/*.ts", "**/*.tsx", "**/*.js"] }