Help us improve
Share bugs, ideas, or general feedback.
From agentsystem-core
Adds empty and error state UIs for data-fetching surfaces. Triggers after wiring queries or loaders to catch missing non-loading states.
npx claudepluginhub agentsystemlabs/core --plugin agentsystem-coreHow this skill is triggered — by the user, by Claude, or both
Slash command
/agentsystem-core:add-empty-error-statesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
A page in three states: loading, empty, error. The fast-path "data shows up" gets all the design attention; the other two get inherited from whatever the renderer falls back to. This skill catches both.
Provides a checklist for code reviews covering functionality, security, performance, maintainability, tests, and quality. Use for pull requests, audits, team standards, and developer training.
Share bugs, ideas, or general feedback.
A page in three states: loading, empty, error. The fast-path "data shows up" gets all the design attention; the other two get inherited from whatever the renderer falls back to. This skill catches both.
In the file the user names (or the most recently edited route/component), find every:
useQuery(...) / useSuspenseQuery(...) (TanStack Query)loader returning data the component readsuseFetcher / useLoaderDataFor each, identify what's rendered with the result. The empty/error treatment goes wherever that result is consumed.
Exit: every data-fetch surface and its render site are listed.
For each fetch, verify the component handles all three of:
| Branch | Trigger | Treatment |
|---|---|---|
| Loading | first render before data resolves | Skeleton (handled by add-skeleton-loaders, out of scope here). |
| Empty | data resolved but is null, undefined, [], or otherwise represents "nothing" | Empty state UI. |
| Error | query/loader threw or returned an error | Error state UI. |
For TanStack Query: check that query.isPending / query.isError / query.data are all reachable in the render. With useSuspenseQuery, errors are caught by the nearest <ErrorBoundary> and loading by <Suspense> — verify both boundaries exist on the route.
For loaders: check the route's errorComponent (TanStack Router) or equivalent; check pendingComponent for loading; the empty case must be handled in the route component itself.
Exit: for each fetch, mark Empty and Error as OK, MISSING, or WEAK.
Search the codebase for an existing empty/error component:
<EmptyState> / EmptyState.tsx — shadcn-style pattern, common in this stack.<NoResults> / <ErrorBoundary> / <Empty>.If a primitive exists, use it. If not, propose the smallest one that matches the project's component style — but flag this as a structural decision and let the user confirm before creating shared components.
Exit: for each missing/weak state, the component to use is identified.
For each MISSING or WEAK state, apply the inline fix:
const { data: posts } = useQuery(postsQueryOptions())
if (posts.length === 0) {
return (
<EmptyState
icon={FileTextIcon}
title="No posts yet"
description="Create your first post to get started."
action={
<Button asChild>
<Link to="/posts/new">New post</Link>
</Button>
}
/>
)
}
The empty state must:
If the empty state means "nothing yet, never will be without action" → lead with the action. If it means "you've filtered everything out" → lead with "clear filters".
if (query.isError) {
return (
<ErrorState
title="Couldn't load posts"
description="Something went wrong. Try again in a moment."
action={<Button onClick={() => query.refetch()}>Retry</Button>}
// In dev, surface the actual error:
details={import.meta.env.DEV ? String(query.error) : undefined}
/>
)
}
The error state must:
error.message to end users (often a stack trace or technical noise).If the route is using useSuspenseQuery, place the error UI in the route's errorComponent (or a <ErrorBoundary> wrapping the suspense boundary), not in the component body.
Exit: each previously MISSING/WEAK state is now OK.
Render the route in the dev browser at three states (the user does this; report the steps):
where(eq(posts.id, '__never__'))) or use a fresh test user with no data.throw new Error('test') in the server fn) — confirm the error state renders, retry works after removing the throw.Exit: the report includes the manual verification steps; the user is told to run them.
Empty/error states added in: <file>
<component name>
Empty state: ✓ added — uses <EmptyState> with <CTA>
Error state: ✓ added — uses <ErrorState> with retry
Verify by:
1. Trigger empty: <how>
2. Trigger error: <how>
3. Confirm happy path still renders normally.
NEVER show raw error.message to end users in the rendered error state.
Instead: a human-readable message like "Couldn't load posts. Try again in a moment." Surface the raw error only in dev (import.meta.env.DEV) or behind a developer-only toggle.
Why: raw errors are usually stack traces or technical strings ("ECONNRESET", "23505: duplicate key value violates unique constraint…"). Showing them confuses users and leaks internal details to anyone who can hit the URL.
NEVER use a generic "Something went wrong" everywhere. Instead: specific to the surface — "Couldn't load posts" / "Couldn't save draft" / "Couldn't load comments". Name the thing that failed. Why: generic messages make every error feel the same. A specific message lets the user understand the scope: "the comments didn't load but the post is fine" is reassuring and actionable; "Something went wrong" is not.
NEVER omit the retry affordance on a retry-safe error.
Instead: include a Retry button that calls query.refetch() (or the loader's reload). Skip retry only on explicitly non-retry-safe errors (4xx that the user must fix themselves).
Why: the most common cause of an error is a transient network blip. Without retry, the user reloads the whole page; with retry, they continue what they were doing.
NEVER render an empty state that's just "No results." Instead: name what's empty and (when applicable) tell the user how to fix it: "No posts yet — create your first one" or "No matching results — clear filters to see all posts." Why: "No results" without context leaves the user wondering whether the page is broken, the filter is too narrow, or they actually have nothing. Specific copy answers the question.
NEVER place the error UI inside useSuspenseQuery's consumer — it never runs there.
Instead: put it in the route's errorComponent or a <ErrorBoundary> wrapping the suspense boundary.
Why: suspense queries throw on error; the throw bubbles up past your component to the nearest error boundary. Code in the consumer that checks query.isError is unreachable.
NEVER conflate "empty" with "loading".
Instead: check the loading branch first (isPending / suspense), only then check empty (data.length === 0). The order matters because data is undefined while loading.
Why: if empty is checked first, every loading state flashes the empty UI for one frame before the real data arrives. Users perceive this as "broken page that fixed itself" — worse than just showing a skeleton.
NEVER add a third UI primitive when the project already has an empty/error component pattern.
Instead: find and reuse the existing EmptyState / ErrorState / equivalent.
Why: duplicate primitives fragment the design system. Reviewers waste cycles deciding which to use; users see inconsistent UI.