Atomic Designを理解する:基礎と応用(React + Todoアプリ構築)
1. Atomic Designとは
概要
Atomic Designは、UIコンポーネントを階層構造で整理するデザインシステムです。Brad Frostによって提唱され、UI設計を分割・再利用しやすくすることを目的としています。特に、大規模プロジェクトやチーム開発で有効です。
Atomic Designの5階層
-
Atoms(原子)
UIの最小単位です。独立して意味を持ちませんが、他のコンポーネントと組み合わせて利用されます。
例: ボタン、入力フィールド、ラベル。 -
Molecules(分子)
複数のAtomsを組み合わせて、単一の機能を持つコンポーネント。
例: ラベル付きの入力フォーム。 -
Organisms(有機体)
MoleculesやAtomsを組み合わせたUIの主要部分を形成します。
例: ナビゲーションバー、カードリスト。 -
Templates(テンプレート)
Organismsを組み合わせてページのレイアウトを構築します。具体的なデータは含まず、骨組みのみを提供します。 -
Pages(ページ)
Templatesに具体的なデータを注入して、完成したページを提供します。
2. Atomic Designのメリット
-
再利用性の向上
各階層が独立しているため、コンポーネントの再利用が容易になります。 -
一貫性の確保
プロジェクト全体で同じコンポーネントを使用するため、UIデザインが一貫します。 -
保守性の向上
階層化により変更が局所化され、保守性が向上します。 -
チーム開発の効率化
各階層ごとに作業を分担しやすく、チーム開発がスムーズに進みます。
3. TodoアプリにおけるAtomic Designの適用
要求仕様
- タスクの追加、完了、削除。
- シンプルでモバイルフレンドリーなUI。
- 再利用可能なコンポーネント設計。
ディレクトリ構成
Atomic Designの階層に基づいてコンポーネントを整理します。
src/
├── components/
│ ├── atoms/
│ │ ├── Button.tsx
│ │ ├── Checkbox.tsx
│ │ ├── InputField.tsx
│ ├── molecules/
│ │ ├── TodoItem.tsx
│ ├── organisms/
│ │ ├── TodoList.tsx
│ ├── templates/
│ │ ├── TodoTemplate.tsx
│ ├── pages/
│ ├── TodoPage.tsx
├── styles/
├── types/
│ ├── todo.ts
├── pages/
│ ├── index.tsx
4. 注意点
1. チームでの意思統一の難しさ
Atomic Designの最大の課題は、チームでの意思統一です。個人開発であれば、どのコンポーネントがどこに格納されているかを自分で把握していれば問題ありません。しかし、チーム開発においてはそうはいきません。プロジェクトに関わる全員が、同じ設計思想とルールを理解して足並みを揃える必要があります。
ルールの明確化
チーム全体で使用するAtomic Designのルールを明確化することが重要です。以下のポイントを明確にしておくと良いでしょう。
- 各層の責務(Atoms, Molecules, Organisms, Templates, Pages)を具体的に定義する。
- どのようなコンポーネントがAtomsに属し、どのようなものがOrganisms以上に含まれるべきかの基準を示す。
- ディレクトリ構成や命名規則を統一する。
ルールを守るための対策
明確なルールがあっても、それを遵守しなければ意味がありません。特に、後からチームに参加するメンバーがルールを理解していない場合、簡単に破綻してしまいます。そのため、次の取り組みが必要です。
- ルールの初期説明: プロジェクト開始時に、Atomic Designのルールや目的をチーム全員に共有し、共通理解を形成します。
- 厳格なコードレビュー: コードレビューを通じて、ルールが守られているかを確認し、必要に応じてフィードバックを行います。
- 教育とドキュメント化: 新しいメンバーが参画した際に迅速に理解できるよう、ルールをドキュメント化し、トレーニングを実施します。
成果
これらを徹底することで、チームの意思統一が図られ、プロジェクト全体が整然とした設計を維持できるようになります。また、結果的にコードの品質向上や保守性の向上にもつながります。
2. 初期設計の重要性
Atomic Designでは、初期設計が非常に重要です。特に、どの要素をAtomsやMoleculesに分類するかの判断を誤ると、プロジェクトが進むにつれて構造が破綻していく危険があります。この問題はAtomic Designに限らず、あらゆるアーキテクチャ設計において共通する課題ですが、Atomic DesignではUIコンポーネントの責務が特に厳密であるため、初期段階での失敗が致命的になることが多いです。
失敗例
- 汎用的であるべきAtomsにビジネスロジックや特定のスタイルを埋め込む。
- 再利用性を高めすぎて、MoleculesやOrganismsが過剰に抽象化されてしまう。
- TemplatesやPagesでのデータ管理ルールが曖昧で、責務が曖昧になる。
初期設計で重視すること
-
UIの全体像を把握する
アプリケーション全体のUI構造を把握したうえで、各層のコンポーネントを洗い出します。 -
責務の明確化
各コンポーネントが担うべき責務を明確にし、ルール化します。 -
柔軟性を持たせる
初期設計時に全てを固定するのではなく、プロジェクトの進行に伴う変更や追加に対応できる柔軟性を残しておきます。
5. Todoアプリの具体的な適用例
Atoms
最小単位のコンポーネントを実装。
- Button: 汎用的なボタン。
- Checkbox: タスクの完了状態を示すチェックボックス。
- InputField: タスクを追加するための入力フィールド。
Button.tsx
import React from 'react';
import styles from './Button.module.css';
type ButtonProps = {
label: string;
onClick: () => void;
};
export const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
<button className={styles.button} onClick={onClick}>
{label}
</button>
);
Checkbox.tsx
import React from 'react';
type CheckboxProps = {
checked: boolean;
onChange: () => void;
};
export const Checkbox: React.FC<CheckboxProps> = ({ checked, onChange }) => (
<input type="checkbox" checked={checked} onChange={onChange} />
);
InputField.tsx
import React from 'react';
type InputFieldProps = {
placeholder?: string;
value: string;
onChange: (value: string) => void;
};
export const InputField: React.FC<InputFieldProps> = ({ placeholder, value, onChange }) => (
<input
type="text"
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
Molecules
複数のAtomsを組み合わせた機能単位。
- TodoItem: タスクの1つ分の表示。ボタンやチェックボックスを含む。
TodoItem.tsx
import React from 'react';
import { Button } from '../atoms/Button';
import { Checkbox } from '../atoms/Checkbox';
type TodoItemProps = {
text: string;
completed: boolean;
onToggle: () => void;
onDelete: () => void;
};
export const TodoItem: React.FC<TodoItemProps> = ({ text, completed, onToggle, onDelete }) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Checkbox checked={completed} onChange={onToggle} />
<span style={{ textDecoration: completed ? 'line-through' : 'none' }}>{text}</span>
<Button label="Delete" onClick={onDelete} />
</div>
);
Organisms
独立した機能を持つUIの大きな塊。
- TodoList: 複数のTodoItemをまとめたリスト。
TodoList.tsx
import React from 'react';
import { TodoItem } from '../molecules/TodoItem';
type Todo = {
id: number;
text: string;
completed: boolean;
};
type TodoListProps = {
todos: Todo[];
onToggle: (id: number) => void;
onDelete: (id: number) => void;
};
export const TodoList: React.FC<TodoListProps> = ({ todos, onToggle, onDelete }) => (
<div>
{todos.map((todo) => (
<TodoItem
key={todo.id}
text={todo.text}
completed={todo.completed}
onToggle={() => onToggle(todo.id)}
onDelete={() => onDelete(todo.id)}
/>
))}
</div>
);
Templates
UI全体のレイアウト。
- TodoTemplate: タスク追加フォームとTodoリストをレイアウト。
TodoTemplate.tsx
import React from 'react';
import { TodoList } from '../organisms/TodoList';
import { InputField } from '../atoms/InputField';
import { Button } from '../atoms/Button';
type TodoTemplateProps = {
todos: { id: number; text: string; completed: boolean }[];
newTodo: string;
onAddTodo: () => void;
onToggleTodo: (id: number) => void;
onDeleteTodo: (id: number) => void;
onNewTodoChange: (value: string) => void;
};
export const TodoTemplate: React.FC<TodoTemplateProps> = ({
todos,
newTodo,
onAddTodo,
onToggleTodo,
onDeleteTodo,
onNewTodoChange,
}) => (
<div>
<div style={{ display: 'flex', marginBottom: '1rem' }}>
<InputField value={newTodo} onChange={onNewTodoChange} placeholder="Add new task" />
<Button label="Add" onClick={onAddTodo} />
</div>
<TodoList todos={todos} onToggle={onToggleTodo} onDelete={onDeleteTodo} />
</div>
);
Pages
具体的なデータを注入。
- TodoPage: 完成したTodoアプリページ。
TodoPage.tsx
import React, { useState } from 'react';
import { TodoTemplate } from '../templates/TodoTemplate';
export default function TodoPage() {
const [todos, setTodos] = useState<{ id: number; text: string; completed: boolean }[]>([]);
const [newTodo, setNewTodo] = useState('');
const handleAddTodo = () => {
if (newTodo.trim()) {
setTodos([...todos, { id: Date.now(), text: newTodo, completed: false }]);
setNewTodo('');
}
};
const handleToggleTodo = (id: number) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const handleDeleteTodo = (id: number) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
return (
<TodoTemplate
todos={todos}
newTodo={newTodo}
onAddTodo={handleAddTodo}
onToggleTodo={handleToggleTodo}
onDeleteTodo={handleDeleteTodo}
onNewTodoChange={setNewTodo}
/>
);
}
6. まとめ
Atomic Designは、再利用性、一貫性、保守性を高めるための強力な設計手法です。特に、Next.jsのようなコンポーネントベースのフレームワークと組み合わせることで、その利点を最大限に引き出せます。
注意点を踏まえつつ、柔軟に適用することで、スケーラブルで効率的なUI設計が可能になります。この記事を通じて、Atomic Designを活用した実践的な設計方法を学び、プロジェクトでの応用に役立ててください。
Discussion
細かい点で失礼します
Atom レベルのコンポーネントについては、自力で型を記述するのではなく、標準の要素の Props をそのまま引用する という書き方のほうが良いです。そうしないと、ボタンとして、入力欄として、備える必要のある機能の一部を失うことになってしまいます。
たとえば、サンプルコードの
Button
コンポーネントは、button 要素の既定値であるtype="submit"
が設定されているので、<form>
の内側に置いていると、望んでいなくても「このフォームの送信ボタン」として機能してしまい、それを抑えるためにはtype="button"
と設定する必要があります。このように、標準の要素に対して設定できる項目を隠してしまうことは、機能不全の原因になります。詳しくは、以下の記事で述べられています。
もう一点、
のように大まかに分けることに意味はあると思いますが、
後者について、"page / template / organism / molecule" といった 決まった数の階層に、無理に押し込む のは、"UI の実際の構造" を無視することになるので、賛同できません。feature ベースな方法で、関連するコンポーネントを一箇所に集めるようなディレクトリ構造にすることをお勧めします。
"UI の実際の構造" を、「React がコンポーネントの状態をどう扱うか」に基づいて観察してコードに落とし込むことについては、以下の記事で述べています。
コメントいただきありがとうございます。
1点目に関しておっしゃる通りだと思います。
ありがとうございます。
2点目に関してですが、こちらはなるほどと言う感じでした。
私も実務だと基本featuresベースな方法で開発を行っているため、記事を書きつつ懐疑的な気持ちはありました。
ただこの懐疑的な部分をあまり言語化できなかったため、表面上で理解したことを文章として書き、コードまで落とし込んでみましたが、言及されてる通りでそうあるべきだと考えてます。