【React】コンポーネント分割の指標Atomic Design

コンポーネントを分割する際の指標となるAtomic DesignについてUdemyのこちらの教材から学んだことをまとめさせていただいています。

Reactに入門した人のためのもっとReactが楽しくなるステップアップコース完全版

Reactに入門した人のためのもっとReactが楽しくなるステップアップコース完全版
「React何となく分かったけど次どうしたら良いか分からない」という人がステップアップするために知っておくべきことを順序立ててハンズオン形式で詰め込みました。本コースを終える頃にはもっとReactのことを好きになっていると思います。

React特有の概念であるコンポーネント分割について、一概に分けるといっても、どれくらいの粒度で分割すればよいのかわからないもの。その一つの指標となるのがAtomicDesign。

AtomicDesign は以下の5つの粒度に分けられる。

粒度が小さい順に

  • Atoms
    それ以上分解できない要素
  • Molecules
    Atomの組み合わせで意味を持つデザインパーツ
  • Organisms
    AtomやMoleculeの組み合わせで構成される単体である程度の意味を持つ要素群
  • Templates
    実際のデータは持たず、ページのレイアウトのみを表現する要素
  • Pages
    最終的に表示される一画面

これはあくまで、コンポーネント分割の判断材料になるものであって必ずしもこうしなければならないといったReactのルールがあるわけではない。

それでは、それぞれについて実装しながら見ていきたいと思います。

Atoms

ここでやること
  • ボタンの作成
  • 作成したボタンの共通使用

・react-router-domをimport

・スタイルを当てるために、styled-componentsをimport

・componentsディレクトリを作成

・components > atoms > button > PrimaryButton.jsxを作成

ここでのポイント!

ここで作ったボタンをいろんなところで使えるようにする必要があるので、

propsを受け取ってボタンを生成できるようにしたい。

propsとしてchildrenを受け取るようにする。

App.jsでPrimaryButtonを呼び出してテストします。

export const PrimaryButton = (props) => {
  const { children } = props;
  return <button>{children}</button>;
};

import { PrimaryButton } from "./components/atoms/button/PrimatyButton";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <PrimaryButton>テスト</PrimaryButton>
    </div>
  );
}

既存のコンポーネントに更にCSSを上書きする方法

“ボタンのベースは同じにしたいけど、色だけ変えたい”といったようなときに、コンポーネント化をすることで実現することができる。

今回は、テキストカラーは共通とし、背景色が異なるような場合を作成する。

共通要素を記述するコンポーネントBaseButton.jsxを作成し、共通CSSを記述

BaseButtonをimport

普通の書き方と違うところ

const SButton = styled.button`

     ↓

const SButton = styled(BaseButton)`

こう書くことで、コンポーネントのBaseButtonのCSSの要素を持ったまま、要素を加えていくことができる。

これで、ボタンのベース要素を共通化することが出来ました。

import styled from "styled-components";
import { BaseButton } from "./BaseButton";

export const PrimaryButton = (props) => {
  const { children } = props;
  return <SButton>{children}</SButton>;
};

const SButton = styled(BaseButton)`
  background-color: blue;
`;

import styled from "styled-components";

export const BaseButton = styled.button`
  color: white;
`;

Molecules

ここでやること

検索フォームとボタンの組み合わせを作成

・components > molecules > SearchInput.jsxを作成

・inputにstyleを当てる
 atom配下にinputディレクトリを作成し、inputコンポーネントを作成

・placeholderはpropsで受け取り動的に変えられるようにする。

const { placeholder = "" } = props;

こう記述することでデフォルトで空文字とすることができる。

できたのがこちら

import { PrimaryButton } from "../atoms/button/PrimatyButton";
import { Input } from "../atoms/input/Input";

export const SearchInput = () => {
  return (
    <div>
      <Input placeholder="検索条件を入力" />
      <PrimaryButton>検索</PrimaryButton>
    </div>
  );
};

import styled from "styled-components";

export const Input = (props) => {
  const { placeholder = "" } = props;
  return <SInput type="text" placeholder={placeholder} />;
};

const SInput = styled.input`
  padding: 8px 16px;
  border: solid #ddd 1px;
`;

Organismsの作成

ここでやること

ユーザーカード を作成する

・components > organisms > user ディレクトリを作成

・user > UserCard.jsx コンポーネントを作成

・プロフィール情報を記述

・imgに、画像を表示できる便利なサービスUnsplashを使う。

画像をURLで表示する方法

・選んだ画像のURLをコピー

・URLを少し編集
 URLをJSXに貼り付ける際にはひと手間必要になる。
 例えばURLが以下の場合、JSXに貼り付ける際に以下のように変更する。
 URL: https://unsplash.com/photos/BJaqPaH6AGQ

<img src="https://source.unsplash.com/BJaqPaH6AGQ" alt="プロフィール" />

以上のように、photosを削除し、source.を追加する。

・styleを当てる

・UserCardにpropsを渡して表示する

・propsで渡すuserをApp.jsに定義する

ここまでのコードがこちら

import { UserCard } from "./components/organisms/user/UserCard";

const user = {
  name: "グール",
  image: "https://source.unsplash.com/BJaqPaH6AGQ",
  email: "12345@example.com",
  phone: "090-1234-5678",
  company: {
    name: "テスト会社"
  },
  website: "https://google.com"
};

export default function App() {
  return (
    <div className="App">
      <UserCard user={user}/>
    </div>
  );
}

import styled from "styled-components";

export const UserCard = (props) => {
  const { user } = props;
  return (
    <div>
      <img height={160} src={user.image} alt={user.name} />
      <p>{user.name}</p>
      <SDL>
        <dt>メール</dt>
        <dd>{user.email}</dd>
        <dt>tel</dt>
        <dd>{user.phone}</dd>
        <dt>会社名</dt>
        <dd>{user.company.name}</dd>
        <dt>WEB</dt>
        <dd>{user.website}</dd>
      </SDL>
    </div>
  );
};

const SDL = styled.dl`
  text-align: left;
  dt {
    float: left;
  }
  dd {
    padding-left: 32px;
    padding-bottom: 8px;
  }
`;

カードも他のところで使われることが考えられるため、コンポーネント化する。

コンポーネント化することで、カードデザインを共通化することができる。

・atoms > card > Card.jsx コンポーネントを作成
 UserCard.jsxの以下の部分をCardコンポーネントにするようなイメージ

    <div>
      <img height={160} src={user.image} alt={user.name} />
      <p>{user.name}</p>
      <SDL>
        <dt>メール</dt>
        <dd>{user.email}</dd>
        <dt>tel</dt>
        <dd>{user.phone}</dd>
        <dt>会社名</dt>
        <dd>{user.company.name}</dd>
        <dt>WEB</dt>
        <dd>{user.website}</dd>
      </SDL>
    </div>

Cardコンポーネントでchildrenとして受け取る。

export const Card = (props) => {
  const { children } = props;
  return <div>{children}</div>;
};

・UserCard.jsx のdivタグをCardに置き換える

・imgとname のセットのコンポーネントを作る。
 components > molecules > user > UserIconWithName.jsx

UserCardの以下の部分を移管する。

<img height={160} src={image} alt={name} />
<p>{name}</p>

UserIconWithNameコンポーネントに置き換えて、imageとnameのpropsを受け取る。

<UserIconWithName image={user.image} name={user.name} />

UserIconWithNameコンポーネントは以下のようになる。

export const UserIconWithName = (props) => {
  const { image, name } = props;
  return (
    <div>
      <img height={160} src={image} alt={name} />
      <p>{name}</p>
    </div>
  );
};

Templatesの作成

ここでやること

headerを持つテンプレートを作成する

・templatesディレクトリを作成

・templates > DefaultLayout.jsxとHeaderOnly.jsxを作成

・header以外の要素はchildrenにまとめる

・styleを当てる

export const HeaderOnly = (props) => {
  const { children } = props;
  return (
    <>
      <div style={{ height: "50px", backgroundColor: "green" }}></div>
      {children}
    </>
  );
};

・App.jsのコンポーネントを囲っているdivタグをHeaderOnlyコンポーネントに置き換える。

import { HeaderOnly } from "./components/templates/HeaderOnly";

省略

export default function App() {
  return (
    <HeaderOnly>
      <PrimaryButton>テスト</PrimaryButton>
      <PrimaryButton>検索</PrimaryButton>
      <SearchInput />
      <UserCard user={user} />
    </HeaderOnly>
  );
}

headerは別のテンプレートでも使用するため、header部分を別のコンポーネントに分ける。

componets > atoms > layout > Header.jsx

・Linkを作成する

import { Link } from "react-router-dom";
import styled from "styled-components";

export const Header = () => {
  return (
    <SHeader>
      <Link to="/">Home</Link>
      <Link to="/users">Users</Link>
    </SHeader>
  );
};

const SHeader = styled.header`
  background-color: green;
`;

import { Header } from "../atoms/layout/Header";

export const HeaderOnly = (props) => {
  const { children } = props;
  return (
    <>
      <Header />
      {children}
    </>
  );
};

・BrowserRouterで囲う
 Linkを付与したコンポーネントのRouteはBrowserRouterで囲う必要があるため。

Appの中のHeaderOnlyコンポーネント の中のHeaderコンポーネントでLinkを使用している。

import { BrowserRouter } from "react-router-dom";
import { HeaderOnly } from "./components/templates/HeaderOnly";

export default function App() {
  return (
    <BrowserRouter>
      <HeaderOnly>
        <UserCard user={user} />
      </HeaderOnly>
    </BrowserRouter>
  );
}

Pagesの作成

ここでやること

Topページと、user一覧を表示するページを作成

・components > pages > Top.jsxとUsers.jsxを作成する

import styled from "styled-components";

export const Top = () => {
  return (
    <SContainer>
      <h2>Topページです</h2>
    </SContainer>
  );
};

const SContainer = styled.div`
  text-align: center;
`;

Usersコンポーネントも同様

・Routerを作成
 src > router > Router.jsx

・AppのBrowserRouterをここに移管

・TopとUsersのpathを作る

import { BrowserRouter, Route, Switch } from "react-router-dom";
import { Top } from "../components/pages/Top";
import { Users } from "../components/pages/Users";

export const Router = () => {
  return (
    <BrowserRouter>
      <Switch>
        <Route exact path="/">
          <Top />
        </Route>
        <Route path="/users">
          <Users />
        </Route>
      </Switch>
    </BrowserRouter>
  );
};

そしてApp.jsのルーティングはここで作ったRouterに置き換える。

export default function App() {
  return (
    <BrowserRouter>
      <HeaderOnly>
        <PrimaryButton>テスト</PrimaryButton>
        <PrimaryButton>検索</PrimaryButton>
        <SearchInput />
        <UserCard user={user} />
      </HeaderOnly>
    </BrowserRouter>
  );
}

     ↓

export default function App() {
  return <Router />;
}

・ユーザーのサンプルを作る
 いくか作るためにユーザー配列を作る。
 そしてその作成したusers配列をmapで展開しUserCardを割り当てる。

import styled from "styled-components";
import { SearchInput } from "../molecules/SearchInput";
import { UserCard } from "../organisms/user/UserCard";

const users = [...Array(10).keys()].map((val) => {
  return {
    id: val,
    name: "グール",
    image: "https://source.unsplash.com/BJaqPaH6AGQ",
    email: "12345@example.com",
    phone: "090-1234-5678",
    company: {
      name: "テスト会社"
    },
    website: "https://google.com"
  };
});
export const Users = () => {
  return (
    <SContainer>
      <h2>Usersページです</h2>
      <SearchInput />
      {users.map((user) => (
        <UserCard key={user.id} user={user} />
      ))}
    </SContainer>
  );
};

const SContainer = styled.div`
  text-align: center;
`;

ここでたくさん並べたカードをレスポンシブにしていくには、gridの使用が便利!詳細は割愛

まとめ

これでAtom~Pageまでを使いこなして、アプリが完成しました。

Atomic Designとは、コンポーネント分割の指標となるものです。

こうすることで、再利用や、保守の面で良くなるので迷ったときはこれを参照しましょう。

以上、ご覧いただきありがとうございました。

コメント