Compound Componentを作ってみた

# Compound Component とは
Compound Componentは、複数のコンポーネントを親が管理して、状態や動作を共有させるパターンです。これにより、子コンポーネントが一体となって動作するようになります。

## Compound Componentを使うメリット
状態や動作を複数の子コンポーネントで共有したい時や、UI要素の再利用性を高めたい時に適しています。複雑な状態管理が必要な場合でも、親コンポーネントが一元的にロジックを処理できるため、コードが簡潔になり、メンテナンスがしやすくなります。また、UIコンポーネント間での依存関係が強い場合に、より直感的で一貫した動作を実現できます。

## Compound Componentを作ってみよう

このセクションでは、シンプルなPostCardコンポーネントをより柔軟で再利用可能なCompound Componentに変換する方法を紹介します。この手法を使うことで、コンポーネントが使用されるページに応じて、ポストカードの内容やスタイリングをカスタマイズできるようになり、アプリケーション内での再利用性が大幅に向上します。

### 元のPostCardコンポーネント
まず、PostCardコンポーネントの基本的なコードを見てみましょう。これが今回、Compound Componentにリファクタリングする元となるコードです。
```tsx
type Post = {
  id: number;
  title: string;
  content: string;
  user: {
    id: number;
    name: string;
  };
};

type PostCardProps = {
  post: Post;
};

export default function PostCard({ post }: PostCardProps) {
  return (
    <div className="flex w-[300px] flex-col gap-2 rounded-md bg-gray-200 p-4">
      <h2 className="text-lg font-semibold">{post.title}</h2>
      <p>{post.content}</p>
      <p className="text-sm">By {post.user.name}</p>
      <div className="flex flex-row gap-2">
        <button className="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full">
          Read More
        </button>
        <button className="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full">
          Comments
        </button>
      </div>
    </div>
  );
}
```

### 悪い例

以下のように、どのページにいるか判断するフラグで処理を分けると可読性が下がります。
また新しいページでPostCardをしようしたい場合、変更を加える点が多くなってしまいます。
```tsx
type Post = {
  id: number;
  title: string;
  content: string;
  user: {
    id: number;
    name: string;
  };
};

type PostCardProps = {
  post: Post;
  isOnProfile: boolean; // ページによって動作を変えるフラグ
  isOnHomePage: boolean; // さらに別のフラグを追加
};

export default function PostCard({ post, isOnProfile, isOnHomePage }: PostCardProps) {
  return (
    <div className="flex w-[300px] flex-col gap-2 rounded-md bg-gray-200 p-4">
      <h2 className="text-lg font-semibold">{post.title}</h2>
      <p>{post.content}</p>
      <p className="text-sm">By {post.user.name}</p>
      <div className="flex flex-row gap-2">
        <button className="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full">
          Read More
        </button>

        {/* isOnProfile と isOnHomePage フラグで処理を分ける */}
        {isOnProfile ? (
          <button className="w-full bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-full">
            Edit Post
          </button>
        ) : isOnHomePage ? (
          <button className="w-full bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-full">
            Share Post
          </button>
        ) : (
          <button className="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full">
            Comments
          </button>
        )}
      </div>
    </div>
  );
}

```

### ステップ１：サブコンポーネントにする部分を判断する

```tsx
export default function PostCard({ post }: PostCardProps) {
  return (
    <div className="flex w-[300px] flex-col gap-2 rounded-md bg-gray-200 p-4">
      
      {/* サブコンポーネント */}
      <h2 className="text-lg font-semibold">{post.title}</h2>

      {/* サブコンポーネント */}
      <p>{post.content}</p>

      {/* サブコンポーネント */}
      <p className="text-sm">By {post.user.name}</p>

      {/* サブコンポーネント */}
      <div className="flex flex-row gap-2">
        <button className="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full">
          Read More
        </button>
        <button className="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full">
          Comments
        </button>
      </div>
    </div>
  );
}
```

### ステップ２：サブコンポーネントにpostデータを渡すためのcontextを作る

contextを作成し、そのcontextのProviderで囲ってあげましょう。
これでpostデータがサブコンポーネントにpropsで渡されなくても使える状態になります。

```tsx
import { createContext } from "react";

// コンテキストの型
type PostCardContext = {
  post: Post;
};

// コンテキスト
const PostCardContext = createContext<PostCardContext | undefined>(undefined);

type Post = {
  id: number;
  title: string;
  content: string;
  user: {
    id: number;
    name: string;
  };
};

type PostCardProps = {
  post: Post;
};

export default function PostCard({ post }: PostCardProps) {
  return (
    {/* コンテキストのプロバイダでpostを渡す */}
    <PostCardContext.Provider value={{ post }}>
      <div className="flex w-[300px] flex-col gap-2 rounded-md bg-gray-200 p-4">
        <h2 className="text-lg font-semibold">{post.title}</h2>
        <p>{post.content}</p>
        <p className="text-sm">By {post.user.name}</p>
        <div className="flex flex-row gap-2">
          <button className="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full">
            Read More
          </button>
          <button className="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full">
            Comments
          </button>
        </div>
      </div>
    </PostCardContext.Provider>
  );
}

```

### ステップ３：サブコンポーネントでcontextを使えるようにフックを作る

カスタムフックを作成して、postデータへアクセスできるようにします。

```tsx
import { createContext, useContext } from "react";

type PostCardContext = {
  post: Post;
};

const PostCardContext = createContext<PostCardContext | undefined>(undefined);

// カスタムフックを作成
function usePostCardContext() {
  const context = useContext(PostCardContext);
  if (!context) {
    throw new Error("usePostCardContext must be used within a PostCard");
  }
  return context;
}

...
```

### ステップ４：サブコンポーネントを作成する

サブコンポーネントを作成し、childrentして受け取るような形を作りましょう。

```tsx
import { Children, createContext, PropsWithChildren, useContext } from "react";

type PostCardContext = {
  post: Post;
};

const PostCardContext = createContext<PostCardContext | undefined>(undefined);

function usePostCardContext() {
  const context = useContext(PostCardContext);
  if (!context) {
    throw new Error("usePostCardContext must be used within a PostCard");
  }
  return context;
}

type Post = {
  id: number;
  title: string;
  content: string;
  user: {
    id: number;
    name: string;
  };
};

// childrenを受け取れるように型を変更
type PostCardProps = PropsWithChildren & {
  post: Post;
};

// chilrenをpropsとして受け取る
export default function PostCard({ children, post }: PostCardProps) {
  return (
    <PostCardContext.Provider value={{ post }}>
      <div className="flex w-[300px] flex-col gap-2 rounded-md bg-gray-200 p-4">
      　{/* childrenとして渡す */}
        {children}
      </div>
    </PostCardContext.Provider>
  );
}

// サブコンポーネント
PostCard.Title = function PostCardTitle() {
  const { post } = usePostCardContext();

  return <h2 className="text-lg font-semibold">{post.title}</h2>;
};

// サブコンポーネント
PostCard.Content = function PostCardContent() {
  const { post } = usePostCardContext();

  return <p>{post.content}</p>;
};

// サブコンポーネント
PostCard.User = function PostCardUser() {
  const { post } = usePostCardContext();

  return <p className="text-sm">By {post.user.name}</p>;
};

// サブコンポーネント
PostCard.Content = function PostCardContent() {
  const { post } = usePostCardContext();

  return <p>{post.content}</p>;
};

// サブコンポーネント
PostCard.Buttons = function PostCardButtons() {
  return (
    <div className="flex flex-row gap-2">
      <button className="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full">
        Read More
      </button>
      <button className="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full">
        Comments
      </button>
    </div>
  );
};

```

### ステップ５：Compound Componentを使って見る

カスタム可能なCompound Componentが作成できました。
それでは使ってみましょう。

表示したい内容を変えたり、表示したい順序を変えることができます。
もし各サブコンポーネントのスタイリングを変えたければスタイル用のpropsを受け取るようにすれば良いでしょう。

```tsx
import "./App.css";
import PostCard from "./components/PostCard";

function App() {
  const post = {
    id: 1,
    title: "Getting Started with Vue.js",
    content:
      "Vue.js is a progressive JavaScript framework for building user interfaces.",
    user: {
      id: 101,
      name: "Alice",
    },
  };

  return (
    <>
      <PostCard post={post}>
        <PostCard.Title />
        {/* <PostCard.Content /> */}
        <PostCard.User />
        {/* <PostCard.Buttons /> */}
      </PostCard>
    </>
  );
}

export default App;

```