【React】Atomic Design入門:コンポーネントを整理する考え方

React

こんにちは、かつコーチです。

前回はディレクトリ構成のベストプラクティスとして、種類別構成・機能別構成・Atomic Design構成の3つを比較しました。

その中でも「粒度ごとにコンポーネントを分類する」という考え方であるAtomic Designは、名前を聞いたことはあっても実際にどう分ければいいのか迷う人が多い設計手法です。

今回は、Atomic Designの基本的な考え方と、Reactプロジェクトで実際に運用するときのコツを解説していきます。

Atomic Designとは?

化学の元素にたとえた設計思想

Atomic Designは、Brad Frost氏が提唱したUI設計の考え方です。

化学における「原子(Atom)→分子(Molecule)→有機体(Organism)」という物質の階層構造を、UIコンポーネントの粒度にたとえています。

Reactで実装する場合、一般的には次の5階層で考えます。

Atoms(原子) → Molecules(分子) → Organisms(有機体) → Templates(テンプレート) → Pages(ページ)

小さな部品から積み上げて、最終的に1つの画面を組み立てていくイメージです。

なぜ粒度を意識する必要があるのか

Reactに慣れてくると、コンポーネントをどんどん分割したくなります。

ただ、闇雲に分割すると「このコンポーネントはボタンなのか、それともボタンを含む検索フォームなのか」といった、粒度の異なるものが同じフォルダに並んでしまいます。

粒度が揃っていないと、どのコンポーネントを再利用すべきか判断しづらくなり、結果として似たようなコンポーネントを何度も作ってしまう、ということが起きます。

Atomic Designは、この「粒度の物差し」を提供してくれる考え方です。

5つの階層を理解する

Atoms:それ以上分解できない最小単位

Atomsは、ボタン・入力欄・ラベル・アイコンなど、それ以上意味のある単位に分解できない最小のパーツです。

// src/components/atoms/Button.tsx

type Props = {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary";
};

export const Button = ({ label, onClick, variant = "primary" }: Props) => {
  return (
    <button className={`btn btn-${variant}`} onClick={onClick}>
      {label}
    </button>
  );
};

Atomsは、他のどのコンポーネントにも依存しない、単体で完結した存在であるのがポイントです。

Molecules:Atomsを組み合わせた小さなまとまり

Moleculesは、複数のAtomsを組み合わせて、1つの機能を持たせたパーツです。

// src/components/molecules/SearchForm.tsx
import { useState } from "react";
import { Button } from "../atoms/Button";
import { TextInput } from "../atoms/TextInput";

type Props = {
  onSearch: (keyword: string) => void;
};

export const SearchForm = ({ onSearch }: Props) => {
  const [keyword, setKeyword] = useState("");

  return (
    <div className="search-form">
      <TextInput value={keyword} onChange={setKeyword} placeholder="検索キーワード" />
      <Button label="検索" onClick={() => onSearch(keyword)} />
    </div>
  );
};

「入力欄」と「ボタン」というAtoms単体では意味を持たなかったものが、組み合わさることで「検索する」という1つの機能になっています。

Organisms:意味のあるUIブロック

Organismsは、Molecules・Atomsを組み合わせて、画面上で1つの独立したセクションとして成立するパーツです。

// src/components/organisms/Header.tsx
import { SearchForm } from "../molecules/SearchForm";
import { Logo } from "../atoms/Logo";

type Props = {
  onSearch: (keyword: string) => void;
};

export const Header = ({ onSearch }: Props) => {
  return (
    <header className="site-header">
      <Logo />
      <SearchForm onSearch={onSearch} />
    </header>
  );
};

ヘッダー、商品一覧カードのリスト、フッターなどが典型的なOrganismsです。

「ページの中の1セクション」として、他の要素と入れ替えても成立する独立性を持っているかどうかが判断基準になります。

Templates:レイアウトの骨組み

Templatesは、Organismsをどう配置するかというレイアウトを定義する階層です。

// src/components/templates/PageLayout.tsx
import { ReactNode } from "react";
import { Header } from "../organisms/Header";
import { Footer } from "../organisms/Footer";

type Props = {
  children: ReactNode;
};

export const PageLayout = ({ children }: Props) => {
  return (
    <div className="page-layout">
      <Header onSearch={() => {}} />
      <main>{children}</main>
      <Footer />
    </div>
  );
};

ここではまだ「実際のデータ」は登場せず、あくまで骨組みだけを定義するのがポイントです。

Pages:実データを流し込んだ最終形

最後のPagesは、Templatesに実際のデータを流し込んで完成させた、ユーザーが実際に目にする画面です。

// src/pages/HomePage.tsx
import { PageLayout } from "../components/templates/PageLayout";
import { ProductList } from "../components/organisms/ProductList";
import { useProducts } from "../hooks/useProducts";

export const HomePage = () => {
  const { products } = useProducts();

  return (
    <PageLayout>
      <ProductList products={products} />
    </PageLayout>
  );
};

APIから取得したデータやルーティングの情報など、「具体的な文脈」が初めて登場するのがPagesの階層です。

つまずきやすいポイント:MoleculesとOrganismsの境界線

「これはどっちだろう」で手が止まる

Atomic Designを実際に運用してみて、私が最初に一番悩んだのが、MoleculesとOrganismsの線引きです。

❌ Before:迷った末に何でもOrganismsに入れてしまう

// src/components/organisms/PriceLabel.tsx
// 本来はAtoms〜Molecules程度の小さな部品なのに、
// 「なんとなく単体で意味がありそう」という理由でOrganismsに置いてしまっている

type Props = {
  price: number;
  originalPrice?: number;
};

export const PriceLabel = ({ price, originalPrice }: Props) => {
  return (
    <div className="price-label">
      <span className="price">¥{price.toLocaleString()}</span>
      {originalPrice && (
        <span className="original-price">¥{originalPrice.toLocaleString()}</span>
      )}
    </div>
  );
};

こうなると、次第に「迷ったらOrganisms」が社内ルールとして定着してしまい、階層分けの意味がなくなっていきます。

✅ After:判断基準を明文化してから分類する

私のチームでは、次のような基準を決めてから運用するようにしました。

  • Atoms:他のコンポーネントに依存しない最小パーツか
  • Molecules:Atomsを2〜3個組み合わせただけの、単機能のまとまりか
  • Organisms:ページの中で「1つのセクション」として認識できる独立性を持つか
// src/components/molecules/PriceLabel.tsx
// 「価格と元値を並べて表示する」という単機能のまとまりなので
// Moleculesに分類する、という基準をチームで共有しておく

type Props = {
  price: number;
  originalPrice?: number;
};

export const PriceLabel = ({ price, originalPrice }: Props) => {
  return (
    <div className="price-label">
      <span className="price">¥{price.toLocaleString()}</span>
      {originalPrice && (
        <span className="original-price">¥{originalPrice.toLocaleString()}</span>
      )}
    </div>
  );
};

大事なのは、正解を厳密に定義することではなく、チーム内で判断基準を言語化して共有することです。

基準さえ揃っていれば、多少の分類の揺れがあっても、レビューで指摘し合いながら精度を上げていけます。

応用:Atomic Designを完璧に適用しなくてもいい

5階層すべてを律儀に守る必要はない

実務でAtomic Designを導入する際によくある誤解が、「5階層すべてをきっちり分けなければならない」というものです。

実際には、Templates階層を省略して「Organismsを直接Pagesで組み立てる」構成にしているプロジェクトも多くあります。

小規模なプロジェクトであれば、Atoms・Molecules・Organismsの3階層だけで十分に効果を発揮します。

Storybookとの相性の良さ

Atomic Designは、UIカタログツールのStorybookと非常に相性が良い設計手法です。

Atoms・Moleculesの単位でStorybookに登録しておくと、デザイナーとエンジニアが「同じ部品カタログ」を見ながらコミュニケーションできるようになります。

デザインシステムを構築するプロジェクトでは、この組み合わせが特に威力を発揮します。

まとめ

この記事のポイント

  • Atomic Designは「Atoms→Molecules→Organisms→Templates→Pages」の5階層でUIの粒度を整理する考え方
  • Atomsは最小単位、Moleculesは単機能のまとまり、Organismsは独立したセクション
  • MoleculesとOrganismsの境界は曖昧になりやすいため、チームで判断基準を言語化しておく
  • 5階層すべてを厳密に守る必要はなく、プロジェクト規模に合わせて簡略化してよい
  • Storybookと組み合わせると、デザイナーとの連携がしやすくなる

次に読むべき記事

コンポーネントの粒度が整理できたら、次はロジックを再利用するための「カスタムフック」の設計パターンを見ていきます。

→ 次の記事:カスタムフックの設計パターン集

タイトルとURLをコピーしました