🟢

【TypeScriptで学ぶReact入門】Atomic Design実践ガイド📙

2023/03/26に公開

タイトルは記事の内容を読み込ませ、chatGPTに作成してもらいました。
強いタイトルですが、Reactの初学者向けにAtomic Designとは何なのか、それに則ってコンポーネントを分割するにはどのようにするのかを解説しています。
後半では実際にAtomic DesignでTodoアプリを作成していますので試してみてください。
(有識者はアドバイスなどがありましたらどうぞよろしくお願いします)

Atomic Designを理解する

  1. React開発者にとってのAtomic Design
  2. Atomic Designとは
  3. Atomic Designの5つのレベル
  4. Atomic Designのメリットとデメリット
  5. React/TypeScriptで作るTodoアプリ

React開発者にとってのAtomic Design

Reactの開発者がAtomic Designを使う理由は、コンポーネントベースの開発に適した設計思想であるためです。
Atomic Designは、UIを5つの階層に分類します。これにより、再利用可能なコンポーネントが作成され、開発効率が向上します。Reactはコンポーネントベースのアプローチを採用しているため、Atomic Designと相性が良いです。
またAtomic Designはモジュラーなデザインシステムを推奨しており、各コンポーネントは独立して機能します。これにより、Reactの開発者はコンポーネントの追加、削除、変更が容易になり、アプリケーションの拡張性が向上します。
さらにUIコンポーネントが小さい単位に分解されていることで、コンポーネント単体でのテストも容易になりますね。

Atomic Designとは

Atomic Designは、デザインシステムを構築するための方法論の一つで、WebデザインやUIコンポーネントの設計において、再利用可能な要素の階層構造を作成することを目的としています。

Webデザインを化学の原子から構成される物質にたとえ、それぞれの要素を組み合わせることで、より大きなレイアウトやページを構築するアプローチを提案するものです。

Atomic Designの基本的な考え方は、デザインシステムを5つのレベル(Atoms, Molecules, Organisms, Templates, Pages)に分割し、それぞれのレベルで再利用可能なコンポーネントを設計し、組み合わせることで、より大きなシステムを構築することです。これにより、UIコンポーネントの再利用性が向上し、デザインの一貫性を維持できるようになります。

Atomic Designの5つのレベル

Atomic Designでは、デザインシステムを以下の5つのレベルに分割します。

  1. Atoms(原子): これらは、デザインシステムの最小単位であり、ボタン、ラベル、入力フィールドなどの基本的なHTML要素を指します。それ自体では機能を持たず、他の要素と組み合わせることで意味を持ちます。
  2. Molecules(分子): 分子は、原子を組み合わせて作られる比較的単純なUIコンポーネントです。例えば、フォームのラベル、入力フィールド、ボタンを組み合わせて検索フォームを作成することができます。
  3. Organisms(有機体): 有機体は、分子や原子を組み合わせて作られる複雑なUIコンポーネントです。これらは、独立した部分として機能することができます。例えば、ヘッダー、フッター、サイドバーなどが含まれます。
  4. Templates(テンプレート): テンプレートは、有機体、分子、原子を組み合わせて作られるページのレイアウトです。テンプレートは、具体的なコンテンツを持たず、ページの構造やデザインの骨格を定義する役割を果たします。これにより、異なるコンテンツを持つページでも一貫したデザインが維持されます。
  5. Pages(ページ): ページは、テンプレートに具体的なコンテンツを入れた最終的なデザインです。これにより、実際のユーザーインターフェースがどのように見えるか、どのように機能するかを確認できます。

具体的にTwitterを例にして分類してみました。
開発者にとって解釈の違いはあるかと思いますが、上のイメージを参考にしてみてください。

Atomic Designのメリットとデメリット

Atomic Designには、以下のようなメリットとデメリットがあります。

メリット:

  1. 再利用性: Atomic Designの階層的なアプローチにより、UIコンポーネントの再利用性が向上します。これにより、開発時間の短縮やコードの整理が可能になります。
  2. デザインの一貫性: Atomic Designでは、デザイン要素が小さな単位で管理されるため、デザインの一貫性が維持されやすくなります。
  3. チームワークの向上: Atomic Designの明確な構造により、チームメンバー間でのコミュニケーションが容易になります。また、新しいメンバーがプロジェクトに参加する際の学習コストも低減されます。

デメリット:

  1. 複雑さの増加: Atomic Designを完全に実装するには、多くのコンポーネントを作成し、それらを管理する必要があります。これにより、プロジェクトの複雑さが増加する場合があります。
  2. 過剰な最適化: Atomic Designは、すべてのプロジェクトに適しているわけではありません。小規模なプロジェクトでは、Atomic Designを適用することで過剰な最適化が生じることがあります。

React/TypeScriptで作るTodoアプリ

Atomic Designの練習として簡単なアプリを作成してみます。
アプリは、ユーザーがタスクを作成、表示、編集、削除することができるシンプルなTodoリストです。データはブラウザのローカルストレージに保存されるため、DBやAPIの利用は不要です。
完成系は以下になります。

具体的な動作は以下になります。

  1. タスクの作成 - ユーザーがタスク名を入力し、追加ボタンを押すことでタスクを作成できるようにします。
  2. タスクの表示 - 作成されたタスクを一覧表示します。
  3. タスクの編集 - タスク名をクリックすると、編集用の入力フィールドが表示され、ユーザーがタスク名を変更できるようにします。
  4. タスクの削除 - タスク名の横にある削除ボタンを押すことで、タスクを削除できるようにします。
  5. タスクの完了 - タスク名の横にあるチェックボックスをオンにすることで、タスクが完了したことを示すことができます。完了したタスクは、テキストに取り消し線が表示されるようにします。

Atomic Designの概念に従って、以下のようにコンポーネントを分類してみましょう:

Atoms(原子)

  • ボタン
  • 入力フィールド
  • ラベル
  • チェックボックス

Molecules(分子)

  • タスク追加フォーム(入力フィールド、追加ボタン)
  • タスクアイテム(チェックボックス、タスク名、編集用入力フィールド、削除ボタン)

Organisms(有機体)

  • タスクリスト(タスクアイテムの集合)

Templates(テンプレート)

  • メインページ(タスク追加フォーム、タスクリスト)

Todoアプリの実装

まずはsrcフォルダ内に、以下のフォルダを作成します。

- components
  - atoms
  - molecules
  - organisms
  - templates

Atomsの作成

まずは、src/components/atomsフォルダに以下のコンポーネントを作成します。

  • Button (Button.tsx)
  • Input (Input.tsx)
  • Label (Label.tsx)
  • Checkbox (Checkbox.tsx)

atoms/Button.tsx

import styled from "styled-components";

type ButtonProps = {
  onClick?: (e: React.MouseEvent) => void;
  action: "add" | "edit" | "delete";
  children: React.ReactNode;
};

export const Button: React.FC<ButtonProps> = ({ onClick, action, children }) => {
  return <CustomButton action={action} onClick={onClick}>{children}</CustomButton>;
};

const CustomButton = styled.button<{ action: "add" | "edit" | "delete" }>`
  background-color: ${({ action }) => {
    switch (action) {
      case "add":
        return "blue";
      case "edit":
        return "orange";
      case "delete":
        return "red";
      default:
        return "black";
    }
}};
color: white;
`;

atoms/Checkbox.tsx

type CheckboxProps = {
  checked: boolean;
  onChange: (checked: boolean) => void;
};

export const Checkbox: React.FC<CheckboxProps> = ({ checked, onChange }) => {
  return (
    <input
      type="checkbox"
      checked={checked}
      onChange={(e) => onChange(e.target.checked)}
    />
  );
};

atoms/Input.tsx

type InputProps = {
  value: string;
  onChange: (value: string) => void;
  placeholder?: string;
  type?: string;
};

export const Input: React.FC<InputProps> = ({ value, onChange, placeholder, type = 'text' }) => {
  return (
    <input
      type={type}
      value={value}
      onChange={(e) => onChange(e.target.value)}
      placeholder={placeholder}
    />
  );
};

atoms/Label.tsx

type LabelProps = {
  children: React.ReactNode;
}

export const Label: React.FC<LabelProps> = ({ children }) => {
  return <label>{children}</label>;
};

Atomsでは単一の目的をもつシンプルなコンポーネントであることを意識してください。コンテキストやデータに依存しないようにするのが基本です。

Moleculesの作成

  • TaskAddForm (TaskAddForm.tsx)
  • TaskItem (TaskItem.tsx)

molecules/TaskAddFrom.tsx

import { useState } from "react";
import { Button } from "../atoms/Button";
import { Input } from "../atoms/Input";
import { Label } from "../atoms/Label";

type TaskAddFormProps = {
  onAdd: (taskName: string) => void;
}

export const TaskAddForm: React.FC<TaskAddFormProps> = ({ onAdd }) => {
  const [taskName, setTaskName] = useState("");

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (taskName.trim()) {
      onAdd(taskName);
      setTaskName("");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <Label>Task Name</Label>
      <Input
        value={taskName}
        onChange={setTaskName}
        placeholder="Enter task name"
      />
      <Button action="add">Add Task</Button>
    </form>
  );
};

molecules/TaskItem.tsx

import { useState } from 'react';
import { Checkbox } from '../atoms/Checkbox';
import { Button } from '../atoms/Button';
import { Input } from '../atoms/Input';

type TaskItemProps = {
  taskName: string;
  isCompleted: boolean;
  onToggleComplete: () => void;
  onUpdateTaskName: (newTaskName: string) => void;
  onDelete: () => void;
}

export const TaskItem: React.FC<TaskItemProps> = ({
  taskName,
  isCompleted,
  onToggleComplete,
  onUpdateTaskName,
  onDelete,
}) => {
  const [isEditing, setIsEditing] = useState(false);
  const [editedTaskName, setEditedTaskName] = useState(taskName);

  const handleSave = () => {
    onUpdateTaskName(editedTaskName);
    setIsEditing(false);
  };

  return (
    <div>
      <Checkbox checked={isCompleted} onChange={onToggleComplete} />
      {isEditing ? (
        <>
          <Input value={editedTaskName} onChange={setEditedTaskName} />
          <Button action='add' onClick={handleSave}>Save</Button>
        </>
      ) : (
        <>
          <span style={{ textDecoration: isCompleted ? 'line-through' : 'none' }}>
            {editedTaskName}
          </span>
          <Button action='edit' onClick={() => setIsEditing(true)}>Edit</Button>
        </>
      )}
      <Button action='delete' onClick={onDelete}>Delete</Button>
    </div>
  );
};

Moleculesでは複数のAtomsを組み合わせて構成します。データや機能に関するロジックを含むことがありますが、複雑な状態管理やサービスへの依存は避けるのがベターであり、再利用可能な状態を目指します。

Organismsの作成

organisms/TaskList.tsx

import { useState } from "react";
import { TaskAddForm } from "../molecules/TaskAddForm";
import { TaskItem } from "../molecules/TaskItem";

type Task = {
  id: number;
  name: string;
  isCompleted: boolean;
}

export const TaskList: React.FC = () => {
  const [tasks, setTasks] = useState<Task[]>([]);

  const addTask = (taskName: string) => {
    setTasks((prevTasks: Task[]) => [
      ...prevTasks,
      { id: Date.now(), name: taskName, isCompleted: false },
    ]);
  };

  const toggleTaskComplete = (taskId: number) => {
    setTasks((prevTasks: Task[]) =>
      prevTasks.map((task: Task) =>
        task.id === taskId ? { ...task, isCompleted: !task.isCompleted } : task
      )
    );
  };

  const updateTaskName = (taskId: number, newTaskName: string) => {
    setTasks((prevTasks: Task[]) =>
      prevTasks.map((task: Task) =>
        task.id === taskId ? { ...task, name: newTaskName } : task
      )
    );
  };

  const deleteTask = (taskId: number) => {
    setTasks((prevTasks: Task[]) => prevTasks.filter((task: Task) => task.id !== taskId));
  };

  return (
    <div>
      <TaskAddForm onAdd={addTask} />
      {tasks.map((task) => (
        <TaskItem
          key={task.id}
          taskName={task.name}
          isCompleted={task.isCompleted}
          onToggleComplete={() => toggleTaskComplete(task.id)}
          onUpdateTaskName={(newTaskName) =>
            updateTaskName(task.id, newTaskName)
          }
          onDelete={() => deleteTask(task.id)}
        />
      ))}
    </div>
  );
};

複数のMoleculesおよびAtomsを組み合わせて構成されます。状態管理やデータフェッチ、外部サービスとの連携など、より複雑なロジックを含むことがあります。

Templatesの作成

components/templates

export const AppTemplate: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  return (
    <div>
      <header>
        <h1>Task Manager</h1>
      </header>
      <main>{children}</main>
    </div>
  );
};

ページの組み立て

最後に、src/App.tsxで作成した各コンポーネントを組み立てて、ページを完成させます。

import { TaskList } from "./components/organisms/TaskList";
import { AppTemplate } from "./components/Templates/AppTemplate";

function App() {
  return (
    <div className="App">
      <AppTemplate>
        <TaskList />
      </AppTemplate>
    </div>
  );
}

export default App;

Atomic Designに沿ってTodoアプリを作成してみました。

ここからはポエム
Atomic Desigんに関する個人的な感想を述べると、一貫性と拡張性を開発にもたらすことができそうな気はしました。
しかしコンポーネントの分割の概念が曖昧なので、コンポーネントをOrganismsに作ったらいいのか、Moleculesに作ったらいいのか迷う場面がチーム開発では出てきそうだなと思いました。その際に明確な基準をチームで定めたとしても個々で迷う場面はやはり出そうだなと。(業務で利用したことないため予想)
調べてみるとやはり問題点は同じで、どうにか脱却しようと様々なアプローチが取られているみたいですね。以下のzennが面白かったです。『Reactのディレクトリ構成でAtomicデザインをやめた話』
https://zenn.dev/brachio_takumi/articles/2ab9ef9fbe4159

Discussion