ジェネリック型で汎用性の高いReactコンポーネントを作成

f:id:golazooo23:20201221182446p:plain

目次

  • はじめに
  • ジェネリック型とは
  • 汎用性の高いコンポーネントを作成
  • おわりに

はじめに

こんにちは、スタメンでエンジニアをしている手嶋です。普段は、React+TypeScriptでフロントエンドメインで開発をしています。開発の中でReactコンポーネント(以下コンポーネント)を共通的に使いたいことが多々あるのですが、その手法の一つとしてジェネリック型が有効だったので、今回紹介したいと思います。

ジェネリック型とは

  • ジェネリック型は一言で表すと、「型を抽象化したもの」です。
  • 定義だけでは分かりにくいので、理解を早めるために例を挙げます。

  • 以下のようにtextとindexをそれぞれstring型、number型で受け取って、返す関数があったとします。

const showText = (text: string): string => {
  return text
}

const showIndex = (index: number): number => {
  return index
}
  • この似たような処理を共通化する際に、ジェネリック型が役立ちます。
  • 以下のように変更することで、関数を呼び出す際に型を指定すれば処理をまとめることができます。
  • 「型のみ異なる」コードを抽象化することで、汎用性を高くすることが出来ました。
const genericFunction<T> = (arg: T): T => {
  return arg
}

genericFunction<string>("hoge") // showText()と同じ
genericFunction<number>(10) // showIndex()と同じ
  • この考えを応用し、Reactで汎用性の高いコンポーネントを作成します。

汎用性の高いコンポーネントを作成

  • 例として以下コードを挙げます。
  • DropDownListコンポーネントは、アプリケーション内で汎用的に使うものだと想定してください。
  • 役割としては、親コンポーネントから受け取ったusersをmapで回して子コンポーネント(DropDownItemコンポーネント)に渡すことです。
  • 親コンポーネントではDropDownで選択した単一のuser名をStateとして管理しています。
  • (スタイルやDropDownItemコンポーネントの中身は割愛しています)

ジェネリック型使用前

//types
export type UserType = {
  key: number;
  name: string;
};

// 親コンポーネント
import React, { useState, useCallback } from 'react';
import DropDownList from 'components/common/DropDownList';
import UserType from 'types/user';

interface Props {
  users: UserType[];
}

const User = (props: Props) => {
  const { users } = props;
  // DropDownで選択した単一のユーザーの名前をStateで管理
  const [userName, setUserName] = useState();
  
  const handleSetUserName = useCallback((name: string) => {
    setUserName(name)
  }, []);

  return (
    <div>
      <span>ユーザー一覧<span>
      <DropDownList users={users} setUserName={handleSetUserName} />
    <div>
  );
};

export default User;

// DropDownListコンポーネント
import React from 'react';
import DropDownItem from 'components/commmon/DropDownItem';
import UserType from 'types/user'

interface Props {
  users: UserType[];
  setUserName: (name: string) => void; //親コンポーネントで管理するStateをセットするAction
}

const DropDownList = (props: Props) => {
  const { users, setUserName } = props;

  return (
    <div>
      {users.map(user => {
        return (
          <DropdownItem
            key={user.key}
            value={user.name}
            setValue={setUserName}
          />
        );
      })}
    <div>
  );
};

export default DropDownList;
  • この状態でも問題なくコードは動作し、ユーザーの名前をDropDownListとして表示できます。
  • しかし、ユーザーの名前以外(例えばNumber型のリスト)を表示したい場合はどうでしょうか。
  • DropDownListコンポーネントのinterfaceが具体的すぎる(users,setUserNameしか許可していない)ので、再利用することができません。
  • アプリケーション内で同じ見た目を実現する際に、コンポーネントを使い回せないのは勿体ないです。
  • 解決策として、以下のようにジェネリック型を使用します。

ジェネリック型使用後

// 親コンポーネント①
import React, { useState, useCallback } from 'react';
import DropDownList from 'components/common/DropDownList';
import UserType from 'types/user';

interface Props {
  users: UserType[];
}

const User = (props: Props) => {
  const { users } = props;
  // DropDownで選択した単一のユーザーの名前をStateで管理
  const [userName, setUserName] = useState();

  const handleSetUserName = useCallback((name: string) => {
    setUserName(name)
  }, []);

  return (
    <div>
      <span>ユーザー一覧<span>
      // DropDownListに対してstring型を指定して呼び出す
      <DropDownList <string> users={users} setListItem={handleSetUserName} />
    <div>
  );
};

export default User;

// 親コンポーネント②
import React, { useState, useCallback } from 'react';
import DropDownList from 'components/common/DropDownList';
import PriceType from 'types/price';

interface Props {
  price: PriceType[];
}

const Price = (props: Props) => {
  const { price } = props;
  // DropDownで選択した単一の価格をStateで管理
  const [price, setPrice] = useState();
  
  const handleSetPrice = useCallback((price: number) => {
    setPrice(price)
  }, []);

  return (
    <div>
      <span>価格一覧<span>
      // DropDownListに対してnumber型を指定して呼び出す
      <DropDownList <number> listItems={price} setListItem={handleSetPrice} />
    <div>
  );
};

export default Price;

//types
export type ListItemType<T> = {
  key: number;
  value: T; //keyを汎用的なvalueという名前に、valueの型はジェネリックに
};

// DropDownListコンポーネント
import React from 'react';
import DropDownItem from 'components/commmon/DropDownItem';
import ListItemType from 'types/common' //汎用的な型に変更

//親コンポーネントから渡した型がTに
interface Props<T> {
  listItems: ListItemType<T>[];
  setListItem: (value: T) => void; //親コンポーネントで管理するStateをセットするAction
}

//親コンポーネントから渡した型がTに
const DropDownList = <T,>(props: Props<T>) => {
  const { listItems, setListItem } = props;

  return (
    <div>
      {listItems.map(listItem => {
        return (
          <DropdownItem<T>
            key={listItem.key}
            value={listItem.value}
            setValue={setListItem}
          />
        );
      })}
    <div>
  );
};

export default DropDownList;
  • 上記のようにDropDownListコンポーネントのinterfaceを抽象的にしてあげることで、複数の(型が違う)親コンポーネントから呼び出す事が可能になりました。
  • DropDownListコンポーネント内で扱うpropsも汎用性の高い名前にすることで、アプリケーション全体で共通利用しやすくなると思います。

おわりに

今回はジェネリック型を用いて、汎用性の高いReactコンポーネントを作成する方法を紹介しました。 是非、コンポーネントを共通化する際の選択肢の一つとして考慮してみてください。

スタメンでは一緒にプロダクト開発を進めてくれる仲間を募集しています! 興味を持っていただいた方は、是非下記の募集ページを御覧ください。

Webアプリケーションエンジニア募集ページ