Skip to content

Instantly share code, notes, and snippets.

@olpeh
Created June 10, 2019 06:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save olpeh/334c39dbe0f3f73393b06b437b2bcd2a to your computer and use it in GitHub Desktop.
Save olpeh/334c39dbe0f3f73393b06b437b2bcd2a to your computer and use it in GitHub Desktop.
RemoteData in TypeScript
import { Status } from './Status';
export const NotAsked: NotAsked = {
status: Status.NotAsked
};
export interface NotAsked {
status: Status.NotAsked;
}
export const Loading: Loading = {
status: Status.Loading
};
export interface Loading {
status: Status.Loading;
}
export interface Success<Data> {
status: Status.Success;
data: Data;
}
export const ErrorFetching: ErrorFetching = {
status: Status.ErrorFetching
};
export interface ErrorFetching {
status: Status.ErrorFetching;
error?: Error;
}
// Yes, this is valid TypeScript :)
export type RemoteData<Data> =
| NotAsked
| Loading
| Success<Data>
| ErrorFetching;
import { FunctionalComponent, h, ComponentChildren, VNode } from 'preact';
import { RemoteData, Success } from 'models/RemoteData';
import { Card } from 'components/common/Card';
import { Status } from 'models/Status';
import { Spinner } from 'components/common/Spinner';
import { Subtitle } from 'components/common/Subtitle';
import { Title } from 'components/common/Title';
export interface Props {
remoteData: RemoteData<unknown>;
children: ComponentChildren;
// For side-effects
onSuccess?: (data: Success<unknown>) => void;
renderError?: (error?: Error) => VNode | null;
tinySpinner?: boolean;
}
export const RemoteDataWrapper: FunctionalComponent<Props> = ({
remoteData,
children,
onSuccess,
renderError,
tinySpinner
}: Props) => {
switch (remoteData.status) {
case Status.NotAsked:
/*
* This remote data was not asked yet
* Most probably this state is not visible if the fetch happens
* in componentDidMount
* Most probably we don't even want to display this, so let's hide it
* The message may be useful for debugging purposes, though
*/
return <span class="visually-hidden">The data has not been fetched yet</span>;
case Status.Loading:
/*
* This remote data is loading but not available yet
* let's render a spinner instead
*/
return <Spinner tiny={tinySpinner} />;
case Status.ErrorFetching:
/*
* Something went wrong when fetching this remote data
* Display the error message to the user
* Might not look nice in every place this might be visible
*/
if (renderError) {
return renderError(remoteData.error);
} else {
return (
<Card>
<Title>Auch! Something went wrong!<Title>
{remoteData.error && (
<Subtitle>{remoteData.error.message}</Subtitle>
)}
<span>Please try again later.<span>
</Card>
);
}
case Status.Success:
/*
* Remote data is now available, so let's render the children
* Additionally, call the onSuccess function if defined, because
* in some cases we want to do some side-effects, such as updating
* the document title when the data is available
*/
if (onSuccess) {
// Commit horrible side-effects
onSuccess(remoteData);
}
// We use children as the successful state
// could have used a render props as well
return <div>{children}</div>;
default:
// Should never happen, but in case this happens, just return null
return null;
}
};
export enum Status {
NotAsked = 'NotAsked',
Loading = 'Loading',
Success = 'Success',
ErrorFetching = 'ErrorFetching'
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment