react-ui-patterns by davila7/claude-code-templates
npx skills add https://github.com/davila7/claude-code-templates --skill react-ui-patterns仅在没有任何数据可显示时显示加载指示器。
// 正确 - 仅在无数据时显示加载
const { data, loading, error } = useGetItemsQuery();
if (error) return <ErrorState error={error} onRetry={refetch} />;
if (loading && !data) return <LoadingState />;
if (!data?.items.length) return <EmptyState />;
return <ItemList items={data.items} />;
// 错误 - 即使有缓存数据也显示加载器
if (loading) return <LoadingState />; // 重新获取时会闪烁!
是否有错误?
→ 是:显示带重试选项的错误状态
→ 否:继续
是否正在加载且没有数据?
→ 是:显示加载指示器(旋转器/骨架屏)
→ 否:继续
是否有数据?
→ 是,且有项目:显示数据
→ 是,但为空:显示空状态
→ 否:显示加载状态(后备方案)
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 使用骨架屏的场景 | 使用旋转器的场景 |
|---|---|
| 已知内容形状 | 未知内容形状 |
| 列表/卡片布局 | 模态框操作 |
| 初始页面加载 | 按钮提交 |
| 内容占位符 | 内联操作 |
1. 内联错误(字段级)→ 表单验证错误
2. 通知提示 → 可恢复的错误,用户可以重试
3. 错误横幅 → 页面级错误,数据仍部分可用
4. 完整错误屏幕 → 不可恢复,需要用户操作
关键:绝不要静默吞掉错误。
// 正确 - 错误始终暴露给用户
const [createItem, { loading }] = useCreateItemMutation({
onCompleted: () => {
toast.success({ title: 'Item created' });
},
onError: (error) => {
console.error('createItem failed:', error);
toast.error({ title: 'Failed to create item' });
},
});
// 错误 - 错误被静默捕获,用户毫不知情
const [createItem] = useCreateItemMutation({
onError: (error) => {
console.error(error); // 用户什么都看不到!
},
});
interface ErrorStateProps {
error: Error;
onRetry?: () => void;
title?: string;
}
const ErrorState = ({ error, onRetry, title }: ErrorStateProps) => (
<div className="error-state">
<Icon name="exclamation-circle" />
<h3>{title ?? 'Something went wrong'}</h3>
<p>{error.message}</p>
{onRetry && (
<Button onClick={onRetry}>Try Again</Button>
)}
</div>
);
<Button
onClick={handleSubmit}
isLoading={isSubmitting}
disabled={!isValid || isSubmitting}
>
Submit
</Button>
关键:在异步操作期间始终禁用触发器。
// 正确 - 加载时按钮被禁用
<Button
disabled={isSubmitting}
isLoading={isSubmitting}
onClick={handleSubmit}
>
Submit
</Button>
// 错误 - 用户可以多次点击
<Button onClick={handleSubmit}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</Button>
每个列表/集合都必须有一个空状态:
// 错误 - 没有空状态
return <FlatList data={items} />;
// 正确 - 显式空状态
return (
<FlatList
data={items}
ListEmptyComponent={<EmptyState />}
/>
);
// 搜索无结果
<EmptyState
icon="search"
title="No results found"
description="Try different search terms"
/>
// 列表尚无项目
<EmptyState
icon="plus-circle"
title="No items yet"
description="Create your first item"
action={{ label: 'Create Item', onClick: handleCreate }}
/>
const MyForm = () => {
const [submit, { loading }] = useSubmitMutation({
onCompleted: handleSuccess,
onError: handleError,
});
const handleSubmit = async () => {
if (!isValid) {
toast.error({ title: 'Please fix errors' });
return;
}
await submit({ variables: { input: values } });
};
return (
<form>
<Input
value={values.name}
onChange={handleChange('name')}
error={touched.name ? errors.name : undefined}
/>
<Button
type="submit"
onClick={handleSubmit}
disabled={!isValid || loading}
isLoading={loading}
>
Submit
</Button>
</form>
);
};
// 错误 - 数据存在时显示旋转器(会导致闪烁)
if (loading) return <Spinner />;
// 正确 - 仅在无数据时显示加载
if (loading && !data) return <Spinner />;
// 错误 - 错误被吞掉
try {
await mutation();
} catch (e) {
console.log(e); // 用户毫不知情!
}
// 正确 - 错误被暴露
onError: (error) => {
console.error('operation failed:', error);
toast.error({ title: 'Operation failed' });
}
// 错误 - 提交期间按钮未被禁用
<Button onClick={submit}>Submit</Button>
// 正确 - 禁用并显示加载状态
<Button onClick={submit} disabled={loading} isLoading={loading}>
Submit
</Button>
在完成任何 UI 组件之前:
UI 状态:
数据与变更:
每周安装量
266
代码仓库
GitHub 星标数
22.6K
首次出现
Jan 25, 2026
安全审计
安装于
opencode215
gemini-cli213
codex208
github-copilot203
claude-code186
cursor172
Show loading indicator ONLY when there's no data to display.
// CORRECT - Only show loading when no data exists
const { data, loading, error } = useGetItemsQuery();
if (error) return <ErrorState error={error} onRetry={refetch} />;
if (loading && !data) return <LoadingState />;
if (!data?.items.length) return <EmptyState />;
return <ItemList items={data.items} />;
// WRONG - Shows spinner even when we have cached data
if (loading) return <LoadingState />; // Flashes on refetch!
Is there an error?
→ Yes: Show error state with retry option
→ No: Continue
Is it loading AND we have no data?
→ Yes: Show loading indicator (spinner/skeleton)
→ No: Continue
Do we have data?
→ Yes, with items: Show the data
→ Yes, but empty: Show empty state
→ No: Show loading (fallback)
| Use Skeleton When | Use Spinner When |
|---|---|
| Known content shape | Unknown content shape |
| List/card layouts | Modal actions |
| Initial page load | Button submissions |
| Content placeholders | Inline operations |
1. Inline error (field-level) → Form validation errors
2. Toast notification → Recoverable errors, user can retry
3. Error banner → Page-level errors, data still partially usable
4. Full error screen → Unrecoverable, needs user action
CRITICAL: Never swallow errors silently.
// CORRECT - Error always surfaced to user
const [createItem, { loading }] = useCreateItemMutation({
onCompleted: () => {
toast.success({ title: 'Item created' });
},
onError: (error) => {
console.error('createItem failed:', error);
toast.error({ title: 'Failed to create item' });
},
});
// WRONG - Error silently caught, user has no idea
const [createItem] = useCreateItemMutation({
onError: (error) => {
console.error(error); // User sees nothing!
},
});
interface ErrorStateProps {
error: Error;
onRetry?: () => void;
title?: string;
}
const ErrorState = ({ error, onRetry, title }: ErrorStateProps) => (
<div className="error-state">
<Icon name="exclamation-circle" />
<h3>{title ?? 'Something went wrong'}</h3>
<p>{error.message}</p>
{onRetry && (
<Button onClick={onRetry}>Try Again</Button>
)}
</div>
);
<Button
onClick={handleSubmit}
isLoading={isSubmitting}
disabled={!isValid || isSubmitting}
>
Submit
</Button>
CRITICAL: Always disable triggers during async operations.
// CORRECT - Button disabled while loading
<Button
disabled={isSubmitting}
isLoading={isSubmitting}
onClick={handleSubmit}
>
Submit
</Button>
// WRONG - User can tap multiple times
<Button onClick={handleSubmit}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</Button>
Every list/collection MUST have an empty state:
// WRONG - No empty state
return <FlatList data={items} />;
// CORRECT - Explicit empty state
return (
<FlatList
data={items}
ListEmptyComponent={<EmptyState />}
/>
);
// Search with no results
<EmptyState
icon="search"
title="No results found"
description="Try different search terms"
/>
// List with no items yet
<EmptyState
icon="plus-circle"
title="No items yet"
description="Create your first item"
action={{ label: 'Create Item', onClick: handleCreate }}
/>
const MyForm = () => {
const [submit, { loading }] = useSubmitMutation({
onCompleted: handleSuccess,
onError: handleError,
});
const handleSubmit = async () => {
if (!isValid) {
toast.error({ title: 'Please fix errors' });
return;
}
await submit({ variables: { input: values } });
};
return (
<form>
<Input
value={values.name}
onChange={handleChange('name')}
error={touched.name ? errors.name : undefined}
/>
<Button
type="submit"
onClick={handleSubmit}
disabled={!isValid || loading}
isLoading={loading}
>
Submit
</Button>
</form>
);
};
// WRONG - Spinner when data exists (causes flash)
if (loading) return <Spinner />;
// CORRECT - Only show loading without data
if (loading && !data) return <Spinner />;
// WRONG - Error swallowed
try {
await mutation();
} catch (e) {
console.log(e); // User has no idea!
}
// CORRECT - Error surfaced
onError: (error) => {
console.error('operation failed:', error);
toast.error({ title: 'Operation failed' });
}
// WRONG - Button not disabled during submission
<Button onClick={submit}>Submit</Button>
// CORRECT - Disabled and shows loading
<Button onClick={submit} disabled={loading} isLoading={loading}>
Submit
</Button>
Before completing any UI component:
UI States:
Data & Mutations:
Weekly Installs
266
Repository
GitHub Stars
22.6K
First Seen
Jan 25, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode215
gemini-cli213
codex208
github-copilot203
claude-code186
cursor172
Tailwind CSS v4 + shadcn/ui 生产级技术栈配置指南与最佳实践
2,600 周安装