Atomic DesignをベースにUIをコンポーネント化して開発を効率化する
はじめに
Reactで開発しているとコンポーネントベースという言葉をよく聞くと思います。
公式ドキュメントには以下のように記載されています。
コンポーネントベース
自分自身の状態を管理するカプセル化されたコンポーネントをまず作成し、これらを組み合わせることで複雑なユーザインターフェイスを構築します。
コンポーネントのロジックは、Template ではなく JavaScript そのもので書くことができるので、様々なデータをアプリケーション内で簡単に取り回すことができ、かつ DOM に状態を持たせないようにすることができます。
UIをコンポーネントベースで作成すると以下のようなメリットがあります。
- 再利用性が高まるため、効率的に開発できるとともにUIのトーン&マナーを統一できる
- 保守性の高いコードが書ける
しかしいざ開発してみるとどのようにUIをコンポーネントに分割すればよいのか分からず、コンポーネントに分割しないでコピペして実装してしまう、みたいなこともあるのではないでしょうか。
今回紹介するAtomic DesignはUIをコンポーネントに分割する時の考え方を提供してくれる設計手法で、上記のような課題を解決するのに役立ちます。
実際のコード
作成したコンポーネントのイメージ画像
styled-componentsについて
また今回はCSSの記述にstyled-componentsというライブラリを使用しています。
styled-componentsはCSS in JSのライブラリで、これを用いることでHTML・CSS・JavaScriptを一つのファイルに記述することができる、つまり見た目(HTML・CSS)と振る舞い(JavaScript)を一つのファイルに閉じ込めることができ、UIのコンポーネント化と非常に相性がよいです。
公式ドキュメント
CSS in JSのライブラリでは他にもemotionが有名ですね。
また最近ではCSS Modulesもデザイナーさんとの協業の観点から良さそうに思います。
Atomic Designとは
Atomic Designは、アメリカのWebデザイナーであるBrad Frost氏が考案・提唱したUIコンポーネントの設計手法で、画面を構成する要素を5つの階層に分類し、それらを組み合わせて最終的な画面を作り上げていくのが特徴です。
Atomic Designで重要な画面を構成する5つの階層は以下になります。
- Atoms(原子)
- Molecule(分子)
- Organism(有機体)
- Templates
- Pages
上記のようにAtomic Designという名前は、その名の通り化学の原子(Atoms)からきています。
原子が他の原子と結合して分子を構成するように、原子コンポーネントと原子コンポーネントを組み合わせて分子コンポーネントを構成し、その繰り返しで最終的な画面を作り上げていくという設計手法です。
Atoms
AtomsはUIコンポーネントの最小単位で、それ以上分解することができないものです。
MoleculesもOrganismsもTemplatesもPagesも全てAtomsに分解できるようになっています。
Atomsの代表例は、inputやbuttonなど基本的なHTML要素です。
InputTextコンポーネントとButtonコンポーネントのコードは以下になります。
import React from 'react';
import styled from 'styled-components';
import theme from 'theme';
type FormProps = {
className?: string;
placeholder: string;
width?: string;
borderRadius?: string;
margin?: string;
value?: string;
onChange?: (e: any) => void;
};
const StyledInputText = styled.input<
Pick<FormProps, 'width' | 'borderRadius' | 'margin'>
>`
appearance: none;
border: 0;
outline: 0;
box-shadow: inset 2px 2px 5px ${theme.main.darker},
inset -5px -5px 10px ${theme.main.brighter};
color: ${theme.main.accent};
font-size: 1rem;
padding: 1rem;
transition: all 0.2s ease-in-out;
width: ${({ width }) => width};
border-radius: ${({ borderRadius }) => borderRadius};
margin: ${({ margin }) => margin};
::placeholder {
text-shadow: 1px 1px 0 ${theme.main.brighter};
}
&:focus {
box-shadow: inset 1px 1px 2px ${theme.main.darker},
inset -1px -1px 2px ${theme.main.brighter};
}
`;
export const InputText: React.FC<FormProps> = ({
className,
placeholder,
width,
borderRadius,
margin = '0 0 0 0',
value,
onChange,
}) => {
return (
<StyledInputText
type="text"
className={className}
placeholder={placeholder}
width={width}
borderRadius={borderRadius}
margin={margin}
value={value}
onChange={onChange}
/>
);
};
import React from 'react';
import styled from 'styled-components';
import theme from 'theme';
type ButtonProps = {
children: React.ReactNode;
className?: string;
width?: string;
borderRadius?: string;
color?: string;
margin?: string;
onClick?: () => void;
disabled?: boolean;
};
const StyledButton = styled.button<
Pick<ButtonProps, 'width' | 'borderRadius' | 'color' | 'margin'>
>`
border: 0;
outline: 0;
box-shadow: -5px -5px 20px ${theme.main.brighter},
5px 5px 20px ${theme.main.darker};
font-size: 1rem;
font-weight: 600;
text-shadow: 1px 1px 0 ${theme.main.brighter};
padding: 1rem;
cursor: pointer;
transition: all 0.2s ease-in-out;
width: ${({ width }) => width};
border-radius: ${({ borderRadius }) => borderRadius};
color: ${({ color }) => color};
margin: ${({ margin }) => margin};
&:hover {
box-shadow: -2px -2px 5px ${theme.main.brighter},
2px 2px 5px ${theme.main.darker};
}
&:active {
box-shadow: inset 1px 1px 2px ${theme.main.brighter},
inset -1px -1px 2px ${theme.main.darker};
}
`;
export const Button: React.FC<ButtonProps> = ({
children,
className,
width,
borderRadius,
color,
margin = '0 0 0 0',
onClick,
disabled,
}) => {
return (
<StyledButton
className={className}
width={width}
borderRadius={borderRadius}
color={color}
margin={margin}
onClick={onClick}
disabled={disabled}
>
{children}
</StyledButton>
);
};
Molecules
MoleculesはAtomsのUIコンポーネントを複数組み合わせて、ユーザの具体的な動機に応える機能の単位でUIをコンポーネント化します。
Moleculesの代表例は、検索フォームです。
上記のInputTextコンポーネントとButtonコンポーネントを組み合わせて作るとしたら、以下のようになると思います。
import React from 'react';
import styled from 'styled-components';
import { InputText } from 'ui/Form';
import { Button } from 'ui/Button';
type SearchFormProps = {
className?: string;
margin?: string;
};
const Wrapper = styled.div<
Pick<SearchFormProps, 'margin'>
>`
display: flex;
align-items: center;
margin: ${({ margin }) => margin};
`;
export const SearchForm: React.FC<SearchFormProps> = ({
className,
margin = '0 0 0 0',
}) => {
return (
<Wrapper className={className} margin={margin}>
<InputText /> // 引数は省略
<Button /> // 引数は省略
</Wrapper>
);
};
Organisms
OrganismsはAtomsやMoleculesを組み合わせて作成します。
Organismsはそれ自体が独立して成立するコンポーネントであることが特徴です。
Organismsの代表例は、ヘッダーです。
上記のSearchFormコンポーネントとLogoコンポーネントを組み合わせて作るとしたら以下のようになると思います。
import React from 'react';
import styled from 'styled-components';
import { SearchForm } from 'hoge/huga';
// Atoms
// 実装は省略
import { Logo } from 'hoge/huga';
type HeaderProps = {
className?: string;
margin?: string;
};
const Wrapper = styled.div<
Pick<HeaderProps, 'margin'>
>`
display: flex;
justify-content: space-between;
align-items: center;
margin: ${({ margin }) => margin};
`;
export const Header: React.FC<SearchFormProps> = ({
className,
margin = '0 0 0 0',
}) => {
return (
<Wrapper className={className} margin={margin}>
<Logo /> // 引数は省略
<SearchForm /> // 引数は省略
</Wrapper>
);
};
Templates
Templatesはレイアウトにのみ責務を持つコンポーネントです。
Templatesはページの雛形であり、聖杯レイアウトのような基本的なレイアウトをコンポーネントとして作成しておくことで、コンテンツとレイアウトそれぞれの責務を分離することができます。
Pages
Pagesはその名の通り実際のページであり、最終的な成果物です。
また、実際のページであるため再利用されることもありません。
Pagesで状態(State)を管理したり、APIと通信したりし、その結果を下の階層(Templates以下)に流し込んでいくのが基本になるかと思います。
実際にプロジェクトに導入してみて思ったこと
Atomic Designを導入してみてコンポーネントが再利用できるようになったことで、開発の効率性が高まりましたし、UIのトーン&マナーの統一や変更も容易になりました。
またデザイナーの方からデザインをもらった時に、コンポーネントに分割するクセがつき、コンポーネントベースの考え方を身に付ける良い訓練になりました。
しかしAtomic Designをそのままプロジェクトに導入すると、逆に開発の効率性を低めるなと思うこともあったので、その点についても書いていきます。
どこまで厳密に適用するか
Atomic Designは5つの要素・階層で構成されていますが、これを厳密に分けていこうとするとかなり大変だと思います。
特に作ろうとするコンポーネントがAtoms、Molecules、Organismsのどれなのかを判断するのが難しい場合もあります。
例えば削除ボタンにゴミ箱のマテリアルアイコンとDeleteの文言が含まれているとします。
マテリアルアイコンはAtomsで定義しており、そうなるとこの削除ボタンはマテリアルアイコンとテキストというAtomsを組み合わせたMoleculesになるのか?それともその役割からAtomsなのか?というのは判断するのが難しいところです。
あくまでAtomic Design導入の目的は開発の効率化やUIのトーン&マナーの統一であることが多いと思うので、Atomsなのか、Moleculesなのかで悩む時間は本質的ではありません。
またUIコンポーネントライブラリ(Material UIなど)を使用している場合、そもそもAtomsやMolecules層のコンポーネントを自前で用意するケースは非常に少ないです。
ですので小・中規模のコードベースの場合やUIコンポーネントライブラリを利用している場合、コンポーネントがAtomic Designの5つの要素・階層のどこに属するかを厳密にしない、つまりAtomic Designの5つの要素・階層でディレクトリを切らない方が良いと思います。(src/atoms、src/moleculesのようにディレクトリを切らない)
おすすめのディレクトリ構成
上記のようにAtomic Designの5つの要素・階層でディレクトリを切らない場合、おすすめは以下のようなディレクトリ構成でこれはstyled-componentsの作者が考案・提唱しているものです。
src
├── App
│ ├── Header
│ │ ├── Logo.js
│ │ ├── Title.js
│ │ ├── Subtitle.js
│ │ └── index.js
│ └── Footer
│ ├── List.js
│ ├── ListItem.js
│ ├── Wrapper.js
│ └── index.js
├── shared
│ ├── Button.js
│ ├── Card.js
│ ├── InfiniteList.js
│ ├── EmojiPicker
│ └── Icons
└── index.js
https://stackoverflow.com/questions/42987939/styled-components-organization
shared
配下に再利用可能なUIコンポーネント群を配置するイメージです。
App
配下にHeader
コンポーネントやFooter
コンポーネントが配置されていますが、こちらもshared
配下に配置して良いと思います。
これくらいざっくりしたディレクトリ構成の方が分かりやすくておすすめです。
最近ではNext.jsを利用するケースが多いですがその場合、pages
配下のディレクトリ構成と対応する形でcomponents
配下にコンポーネントを配置していくケースが多いと思うので、components/shared
やcomponents/common
のようなディレクトリを切ってそこに再利用可能なUIコンポーネント群を配置するのが良いのではないでしょうか。
またshared
配下のディレクトリ構成についてはUIコンポーネントライブラリのディレクトリ構成が非常に参考になります。
再利用可能なUIコンポーネントを作る時の参考にもなるので、一度リポジトリを覗いてみることをおすすめします。
Atomic Designを勉強するのにおすすめの本
Atomic Designについては以下の書籍が大変参考になりました。
Atomic Design ~堅牢で使いやすいUIを効率良く設計する
Atomic Designを勉強するのにおすすめの記事
styled-componentsの使い方については以下の記事が分かりやすくまとまっていて便利です。
styled-componentsの使い方が分かりやすくまとまってる記事
おわりに
実際にプロジェクトに導入してみて効果は感じたので、最初は少し工数がかかるかもしれませんが、ぜひ皆さんもAtomic Designとstyled-componentsでUIをコンポーネント化してみてください。
Discussion