112
Help us understand the problem. What are the problem?

posted at

updated at

【React / Next】関数コンポーネント(Functional Component)のpropsをジェネリクスにして汎用的に使いたい

やりたいこと

  • TypeScriptで共通コンポーネントを作成する際、抽象度を上げて汎用的に使いたい
  • ジェネリクスを使って汎用性を上げる

想定ケース

  • テーブルコンポーネントを作成して、共通部品として使用する
  • 共通コンポーネントへは、「レコードデータ / カラムデータ / ヘッダー情報」 を props で渡す
    • 今回はヘッダー情報のみ、フロントで保持する

コード

型情報

types/table.ts
type Props<T> = Partial<T> & {
  width?: string | number;
};

// ヘッダーのlabelと幅を定義、さらにデザイン済みのReactNodeを含める
type ColumnObject<T> = {
  headerLabel: string;
  headerWidth?: string | number;
  // itemの幅はヘッダーの幅と同一とするため、headerWidthの値を使用
  item: (_props: Props<T>) => React.ReactNode;
};

// fetchしたデータ以外に「編集ボタン」など、UIにしかないカラムを定義
type ColumnsKey<T> = keyof T | 'editButton';

// 表示させるカラムkeyと、そのkeyに連動した中身を定義
export type Columns<T> = {
  [_key in ColumnsKey<T>]?: ColumnObject<T>;
};

共通コンポーネント側

CommonTable.tsx「通常のfunctionで記述したケース」
// 共通コンポーネントなので、型定義は全てジェネリクス
interface Props<T> {
  columns: Columns<T>;
  records: T[];
}

// function Component<T>(props: Props<T>) {}  の記述にすることで、共通コンポーネントを使用する側で、型を指定できる
export function CommonTable<T>({ columns, records }: Props<T>) {
  // 省略
  return (
    <>
      <div className={styles.head}>{headers}</div>
      {records.map((record) => (
        <div className={styles.record}>{record}</div>
      ))}
    </>
  );
}
CommonTable.tsx「アロー関数で記述したケース」
// 共通コンポーネントなので、型定義は全てジェネリクス
interface Props<T> {
  columns: Columns<T>;
  records: T[];
}

// 使用側で型を指定
export const CommonTable = <T,>({ columns, records }: Props<T>) => {
  // 省略
  return (
    <>
      <div className={styles.head}>{headers}</div>
      {records.map((record) => (
        <div className={styles.record}>{record}</div>
      ))}
    </>
  );
}

関数のジェネリクスについては、筆者が以前に以下の記事を書いたので、もしご興味がございましたらご覧ください。

使用側

pages/list.ts
export const UserList = () => {
  // 省略

  const columns = getItems();

  return (
    {/* 共通コンポーネント使用時に、型を指定 */}
    <CommonTable<FetchTableData>
      records={data}
      columns={columns}
    />
  )
}
items.tsx
// デザイン済みのitem
export const getItems = (): Columns<FetchTableData> => {
  // 省略
  return {
    title: {
      headerLabel: 'title',
      headerWidth: '50%',
      item: ({ title, width }) => (
        <Text flexBasis={width}>
          {title}
        </Text>
      ),
    },
    createdAt: {
      headerLabel: 'created_at',
      headerWidth: '20%',
      children: ({ createdAt, width }) => (
        <Text flexBasis={width}>
          {getFormattedDate(createdAt, 'yyyy年MM月dd日')}
        </Text>
      ),
    },
  };
};

まとめ

  • 共通コンポーネント側
    • Functional Component定義時に、関数ジェネリクスで記述する
    • 例:function Component<T>(props: Props<T>) {}
    • アロー関数での記述例:const Component = <T,>(props: Props<T>) {}
  • 使用側
    • 共通コンポーネント使用時に、型を指定
    • 例: <Common<FetchData> />

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
112
Help us understand the problem. What are the problem?