mcpField
R
react-ui-patterns

react-ui-patterns

github.com/liuchiawei開発238 回インストール36 週間前に更新
リポジトリ

react-ui-patterns について

データ駆動型の React コンポーネントを開発する人向けの UI 状態パターン集です。ローディング、エラー、空状態、非同期操作を扱い、画面のフィードバックを明確にします。

主な機能

  1. 01データがある間は古いローディング表示を避ける
  2. 02再試行可能なエラー表示
  3. 03非同期処理中のボタン無効化
  4. 04コレクションの空状態設計
  5. 05内容に応じたスケルトンとスピナーの選択
  6. 06ローディング・エラー・成功状態の確認

活用シーン

  1. 01クエリ再取得中のローディング処理
  2. 02フォームエラーのユーザーへの表示
  3. 03検索結果なしや空リストの画面設計
  4. 04コンポーネントのデータ状態のテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/liuchiawei/agent-skills --skill react-ui-patterns

インストール後、Claude Code で/react-ui-patternsを入力して読み込まれたか確認します。

詳細

React UI パターン

基本原則

  1. 古い UI を表示しない - ローディングスピナーは実際に読み込み中の場合にのみ表示する
  2. エラーを必ず表示する - 何かが失敗したとき、ユーザーがそれを確実に認識できるようにする
  3. 楽観的更新 - UI が即座に反応しているように感じさせる
  4. 段階的な開示 - コンテンツが利用可能になった時点から順に表示する
  5. 優雅な機能低下 - データが一部しかなくても、まったくないよりは良い

ローディング状態のパターン

黄金律

表示するデータが一切ない場合にのみローディングインジケーターを表示する。

// 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)
スケルトンとスピナーの使い分け
スケルトンを使う場合スピナーを使う場合
コンテンツの形状が事前にわかっているコンテンツの形状が事前にわからない
リスト/カードのレイアウトモーダル内のアクション
初回のページ読み込みボタンの送信
コンテンツのプレースホルダーインライン操作

エラー処理のパターン

エラー処理の階層
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
エラーは必ず表示する

重要: エラーを黙って握りつぶさないこと。

// 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>
処理中はボタンを無効化する

重要: 非同期処理の実行中は、必ず起動元のトリガーを無効化すること。

// 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>

空状態

空状態の要件

すべてのリスト/コレクションには、必ず空状態を用意すること:

// 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>

チェックリスト

UI コンポーネントを完成させる前に:

UI の状態:

  • エラー状態を処理し、ユーザーに表示している
  • ローディング状態は、データが存在しない場合にのみ表示している
  • コレクションには空状態を用意している
  • 非同期処理の実行中はボタンを無効化している
  • 必要に応じて、ボタンにローディングインジケーターを表示している

データとミューテーション:

  • ミューテーションに onError ハンドラーがある
  • すべてのユーザー操作にフィードバック(トースト/視覚的な表示)がある

他のスキルとの連携

  • graphql-schema: 適切なエラー処理を伴うミューテーションのパターンを使用する
  • testing-patterns: すべての UI 状態(ローディング、エラー、空、成功)をテストする
  • formik-patterns: フォーム送信のパターンを適用する

コメント