🍣

Reactと関数の関係から良いコンポーネントを考える

2024/10/15に公開

Reactと関数の関係

Reactは、純関数であることを仮定して設計されているUIライブラリです。

https://ja.react.dev/learn/keeping-components-pure

であれば、Reactのpropsは関数の引数、ReactのUIは関数の戻り値と考えることができると思います。
したがって、良い関数の定義と良いコンポーネントの定義は似ているのではないかという仮説を軸に、良いコンポーネントを考察します。

良い関数の定義から導いた良いコンポーネント

  • function定義
  • propsを明示的に書く
  • コンポーネント名から何を表示するかわかる
  • コンポーネントが単一の役割を持つ
  • propsが多すぎない

順に解説します。

function定義

Reactではコンポーネントをfunctionで定義するか、アロー関数で定義するかの選択肢があります。
僕は、Reactは純関数であることを仮定して設計されているということからfunctionを使ったほうが明示的に関数と示せる点で良いかと考えています。

また、functionを使ったほうが良いと思う理由には以下の点があります。

  • わずかにfunctionのほうがパフォーマンスが良い
  • ReactやVercelなどの公式サンプルコードではfunctionを採用している

function定義とアロー関数についての詳細な解説は、以下の記事をご参照ください。

https://zenn.dev/rgbkids/books/0b75c912d8ac70/viewer/44a959

propsを明示的に書く

関数では、引数を明示的に示しているので、propsの中身を明示的に書いたほうが関数に近づくと思いました。

以下が例です。

明示的にpropsを書いた例
import React from 'react';

interface MyComponentProps {
  name: string;
  age: number;
}

function MyComponent({ name, age }: MyComponentProps) {
  return (
    <div>
      <p>Name: {name}</p>
      <p>Age: {age}</p>
    </div>
  );
}

export default MyComponent;

propsをまとめて扱った例
import React from 'react';

interface MyComponentProps {
  name: string;
  age: number;
}

function MyComponent(props: MyComponentProps) {
  return (
    <div>
      <p>Name: {props.name}</p>
      <p>Age: {props.age}</p>
    </div>
  );
}

export default MyComponent;

前者のほうが可読性が高いと思いますし、不要なpropsの混入を防げるのが良いです。

コンポーネント名から何を表示するかわかる

コンポーネント名がわかりやすいと、そのコンポーネントが何を表示するのか簡単にイメージできます。

例えば、ListComponentという名前だと、何のリストを表示するのかが不明瞭です。
しかし、UserListと命名すれば、そのコンポーネントが「ユーザーのリスト」を表示することが明確になります。これにより、他の開発者や自分がコードを見たときに理解しやすくなり、メンテナンス性が向上します。

コンポーネントが単一の役割を持つ

コンポーネントも関数と同様に、単一の役割を持たせることが重要です。
そのため、適切にコンポーネントを分割する必要があります。また、これにより前述の「コンポーネント名から何を表示するかわかる」にも繋がり、より具体的で意味のあるコンポーネント名を付けることができるようになります。

例として、以下のように1つのコンポーネントに複数の役割を持たせると、複雑で管理しにくくなります。

複数の役割を持つコンポーネント
function UserProfile() {
  const [showDetails, setShowDetails] = React.useState(false);

  const handleToggle = () => {
    setShowDetails(!showDetails);
  };

  return (
    <div>
      <h2>User Profile</h2>
      {/* ユーザー情報を表示 */}
      <div>
        <p>Name: John Doe</p>
        <p>Email: john.doe@example.com</p>
      </div>

      {/* ログインフォーム (別の役割を担う部分) */}
      <form>
        <input type="text" placeholder="Username" />
        <input type="password" placeholder="Password" />
        <button type="submit">Login</button>
      </form>

      {/* ユーザー詳細の表示を切り替える */}
      <button onClick={handleToggle}>
        {showDetails ? 'Hide Details' : 'Show Details'}
      </button>

      {/* ユーザーの詳細情報 */}
      {showDetails && (
        <div>
          <p>Address: 123 Main St</p>
          <p>Phone: 555-1234</p>
        </div>
      )}
    </div>
  );
}

役割を分割したコンポーネント
// ユーザー情報の表示に特化したコンポーネント
function UserInfo({ name, email }: { name: string; email: string }) {
  return (
    <div>
      <p>Name: {name}</p>
      <p>Email: {email}</p>
    </div>
  );
}

// ログインフォームの表示に特化したコンポーネント
function LoginForm() {
  return (
    <form>
      <input type="text" placeholder="Username" />
      <input type="password" placeholder="Password" />
      <button type="submit">Login</button>
    </form>
  );
}

// ユーザー詳細の表示に特化したコンポーネント
function UserDetails({ address, phone }: { address: string; phone: string }) {
  return (
    <div>
      <p>Address: {address}</p>
      <p>Phone: {phone}</p>
    </div>
  );
}

// 親コンポーネントで、他のコンポーネントを組み合わせて使用
function UserProfile() {
  const [showDetails, setShowDetails] = React.useState(false);

  const handleToggle = () => {
    setShowDetails(!showDetails);
  };

  return (
    <div>
      <h2>User Profile</h2>
      {/* ユーザー情報 */}
      <UserInfo name="John Doe" email="john.doe@example.com" />

      {/* ログインフォーム */}
      <LoginForm />

      {/* ユーザー詳細の表示切り替え */}
      <button onClick={handleToggle}>
        {showDetails ? 'Hide Details' : 'Show Details'}
      </button>

      {/* ユーザーの詳細情報を表示 */}
      {showDetails && <UserDetails address="123 Main St" phone="555-1234" />}
    </div>
  );
}

コンポーネント分割のメリット

  1. テストが容易になる
    各コンポーネントが単一の役割を持つため、個別にテストしやすくなります。

  2. 可読性・再利用性の向上
    役割ごとにコンポーネントが分かれていることで、コードの可読性が高まり、再利用も容易になります。

  3. メモ化がしやすい
    コンポーネントごとにメモ化を行うことで、パフォーマンスの向上が期待できます。
    例えば、ログインフォームの操作によってページ全体が再レンダリングされるのを防ぎ、ログインフォームだけを効率的に再レンダリングさせることが可能になります。

  4. propsの数を減らす事ができる
    後述する「propsが多すぎない」の項目で説明します。

propsが多すぎない

propsが少ないということはコンポーネントが分割されているということであり、前述した「コンポーネント分割のメリット」を享受できるということでもあります。

propsを少なくするためのテクニックを紹介します。

  • propsをオブジェクトにする
  • コンポーネントを分割する
  • デフォルト値を使う

1. propsをオブジェクトにする

以下のようにまとめて問題ない値はpropsをオブジェクトにして良いと思います。

// 悪い例
<UserProfile name="John" age={30} email="john@example.com" />

// 良い例
const user = { name: 'John', age: 30, email: 'john@example.com' };
<UserProfile user={user} />

2. コンポーネントを分割する

「単一の役割を持つ」でも紹介しましたが、コンポーネントを分割することで、propsを減らすこともできます。

// 悪い例(多くのpropsを受け取るコンポーネント)
function UserCard({ name, age, email, address, phoneNumber }) {
  // 処理
}

// 良い例(サブコンポーネントに分割)
function UserCard({ user }) {
  return (
    <div>
      <UserInfo name={user.name} age={user.age} />
      <UserContact email={user.email} phoneNumber={user.phoneNumber} />
    </div>
  );
}

3. デフォルト値を使う

以下の例のようにすると、propsを渡さなかったときに、デフォルト値を使うことができます。
ボタンやチェックボックスやラベルなどのアトム層のコンポーネントでデフォルトの大きさを指定するときなどに頻出です。

function Button({ color = 'blue', label = 'Click me' }) {
  return <button style={{ color }}>{label}</button>;
}

<テクニック紹介>似たUIのpropsの条件分岐

これまでコンポーネントを分割することを推奨してきましたが、UIがほぼ一緒の場合、propsの条件分岐の方が管理が楽な場合もあります。

propsの条件分岐を用いる際、表示される内容が明確になるテクニックを紹介します。
例えば、型システムを活用したタグ付きユニオンを使うと、何が表示されるかを明示的にして、可読性を向上させることができます。

以下の例では、ユーザーアイテムや商品アイテムを表示するコンポーネントを定義しています。それぞれのpropsを明示的に分けることで、コンポーネントのイメージがつきやすくなっています。

import React from 'react';

// ユーザーアイテムのprops
interface UserListItemProps {
  type: 'user';
  userName: string;
}

// 商品アイテムのprops
interface ProductListItemProps {
  type: 'product';
  productName: string;
}

// 2つのタイプをまとめた型を定義
type ListItemProps = UserListItemProps | ProductListItemProps;

// アイテムコンポーネントの実装
function ListItem(props: ListItemProps) {
  const name = props.type === 'user' ? props.userName : props.productName;
  return (
    <div>
      <h3>{name}</h3>
    </div>
  );
}

// Appコンポーネント例: 2つのタイプのアイテムをリスト表示
function App() {
  const items: ListItemProps[] = [
    { type: 'user', userName: 'John Doe' },
    { type: 'product', productName: 'Laptop' },
    { type: 'user', userName: 'Jane Smith' },
    { type: 'product', productName: 'Smartphone' },
  ];

  return (
    <div>
      <h1>Item List</h1>
      {items.map((item, index) => (
        <ListItem key={index} {...item} />
      ))}
    </div>
  );
}

export default App;

上記の例では、nameに値を入れて使用することもできますが、あえてtypeというタグで分岐しています。これにより、どんなコンポーネントかを明示的に示すことができます。

また、UserListItemProductListItemでコンポーネントを分けてもよいですが、UIがほぼ一緒で、条件分岐も複雑でない場合、上記のようにpropsの型を利用したUI分岐のほうが管理が楽です。

最後に

この記事では、Reactと関数の関係から良いコンポーネント設計について考察しました。
Reactの公式ドキュメントには、コンポーネントやpropsの設計についての具体的な指針が明確に示されていないため、このような考察を行うことは有意義だと感じています。

これからも、より良いReactのコードを書けるよう、引き続き設計や実装について考察していきたいと思います。

エックスポイントワン技術ブログ

Discussion