Skip to content

LoadingWrapper

LoadingWrapper is a generic component that renders one of three states based on async operation status: a loading indicator, an error state, or the success content. It removes the repetitive if (loading) … else if (error) … pattern from every data-fetching component.

import { LoadingWrapper } from "@tsd-ui/core";
function UserList() {
const { data, isFetching, error } = useQuery(/* ... */);
return (
<LoadingWrapper isFetching={isFetching} fetchError={error}>
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</LoadingWrapper>
);
}

When isFetching is true, a centered PatternFly Spinner is shown. When fetchError is truthy, a default error EmptyState is shown. Otherwise, children are rendered.

PropTypeDefaultDescription
isFetchingbooleanWhether data is currently loading. Takes priority over fetchError.
fetchErrorTError | null | undefinedundefinedThe error object, if any. Rendered when isFetching is false and this is truthy.
isFetchingStateReact.ReactNodeCentered SpinnerCustom loading indicator to replace the default spinner.
fetchErrorState(error: TError) => React.ReactNodeDefaultErrorStateRender function for custom error UI. Receives the typed error.
childrenReact.ReactNodeThe success content, rendered when not loading and no error.

LoadingWrapper is generic over the error type. TypeScript infers TError from fetchError, so your fetchErrorState callback gets the correct type:

interface ApiError {
status: number;
message: string;
}
<LoadingWrapper<ApiError>
isFetching={isFetching}
fetchError={error}
fetchErrorState={(err) => (
// err is ApiError here
<Alert variant="danger" title={`${err.status}: ${err.message}`} />
)}
>
<Content />
</LoadingWrapper>

The component evaluates state top-down with this priority:

  1. isFetching is true → render isFetchingState (or default spinner)
  2. fetchError is truthy → render fetchErrorState(error) (or default error state)
  3. Otherwise → render children

This means loading always wins. If you’re refetching in the background and want to show stale data instead of a spinner, pass isFetching={false} during refetches.

A PatternFly Spinner centered with Bullseye:

<Bullseye>
<Spinner />
</Bullseye>

A PatternFly EmptyState with danger styling:

  • Icon: ExclamationCircleIcon
  • Title: “Unable to connect”
  • Body: “There was an error retrieving data. Check your connection and try again.”
  • Variant: sm

Replace the spinner with a skeleton, a progress bar, or anything else:

<LoadingWrapper
isFetching={isFetching}
fetchError={error}
isFetchingState={<Skeleton screenreaderText="Loading data..." />}
>
<Content />
</LoadingWrapper>

Provide a render function to access the error object and build a contextual error UI:

<LoadingWrapper
isFetching={isFetching}
fetchError={error}
fetchErrorState={(err) => (
<EmptyState
status="danger"
titleText={err.message}
icon={ExclamationTriangleIcon}
>
<EmptyStateBody>
<Button variant="link" onClick={retry}>Try again</Button>
</EmptyStateBody>
</EmptyState>
)}
>
<Content />
</LoadingWrapper>

For pages that fetch multiple independent resources, nest wrappers so each section handles its own async state:

function Dashboard() {
const users = useQuery(/* users */);
const stats = useQuery(/* stats */);
return (
<Grid>
<GridItem>
<LoadingWrapper isFetching={users.isFetching} fetchError={users.error}>
<UserTable data={users.data} />
</LoadingWrapper>
</GridItem>
<GridItem>
<LoadingWrapper isFetching={stats.isFetching} fetchError={stats.error}>
<StatsPanel data={stats.data} />
</LoadingWrapper>
</GridItem>
</Grid>
);
}