はじめに
こんにちは、masa08です。普段はエンジニア限定シェアハウス「GAOGAO-TOKYO」で過ごしつつ、スタートアップで業務委託エンジニアをしつつ、大学生してます(早く卒業したい)。
今年は多くの新しい技術に触れた年でした。特にフロントエンドに関しては知見を得たことが多かったです。去年の今ごろはどうにかしてJavaScriptがかけるくらいの実力だったのですが、今年一年通じて、TypeScriptやReact、Atomic Designと様々な技術や考え方に出会い、実務を通じて自分の理解を深めることができました。
今回の記事ではアウトプットも兼ねて、Atomic Designに沿ったディレクトリ構成でReact×TypeScriptを用いてTodoアプリを作っていきたいと思います。自分自身わからないことも多いので、何かあればご指摘いただけると幸いです。
対象者
- JavaScriptの基本的な知識を持っている
- Reactの基本的な知識を持っている
- Atomicデザインに触れたいと考えている
- TypeScriptに触れたいと考えている
この記事を通じて得られること
- Reactでアプリケーションを作る方法
- TypeScriptでコードを書く経験
- Atomic Designに関する知識
Atomic Designとは
画像引用元サイト: [Atomic Design Methodology](http://atomicdesign.bradfrost.com/chapter-2/)Atomic Designとは、Webサイト上に存在するものすべてをコンポーネントとみなし、粒度ごとにコンポーネントの種類を分けて管理していく、コンポーネントベースの開発手法のことです。小さいコンポーネントを組み合わせて、より大きなコンポーネントを作っていきます。以下の5つの粒度に分けて、コンポーネントを大別します。この際、上位層は下位層に依存します。
- Atoms(原子)
- Molecules(分子)
- Organisms(有機体)
- Templates(テンプレート)
- Pages(ページ)
Atoms
Atoms層は、それ以上UIとして分解できない最小要素のことです。テキストやボタンが代表的な要素です。
Molecules
Molecules層は、2つ以上ののAtomsが組み合わされて作られたコンポーネントです。検索フォームなどが代表的な要素です。
Organisms
Organisms層はMoleculesやAtomsで構成されるコンポーネント群です。Headerなどが代表的な要素です。
Templates/Pages
Template層はページの雛形、Pages層は実際のページ、すなわちTemplate層にコンテンツを流し込んだものになります。ここにきてユーザーから見えるページが完成します。organisms層を中心として、Templates/Pagesを組み立てていきます。
Atomic Designを採用するメリット
従来の開発手法と比べて、コンポーネントベースでのUI開発には以下の利点があります。
- アプリケーションのメンテナンスしやすくなる
- 解決する問題が小さくなる(責務の分離)
- チーム内で共通認識を持つことができる
- コンポーネントの再利用、平行開発等にによって、開発のスピードが速くなる
実際にコードを書いて、Atomic Designの雰囲気を感じてみましょう。
参考サイト
Atomic Design について調べて見た
Atomic Designを分かったつもりになる
React, Components, and Design
Todoアプリを作る
Atomic Designがどのようなものか、atomsとmolecules、organisms、pagesを作る過程を追いながら確認していきます。
環境を整える
まず最初にnodeをインストールしましょう。以下のサイトからダウンロードします。
https://nodejs.org/ja/download/
ダウンロードしたファイルを開いて、nodeをインストールします。インストールが終わったら、ターミナルで以下のコマンドを打って、nodeがインストールされているのかを確認しましょう。またnodeをインストールするとnpmを一緒にインストールされるので、そちらも確認しましょう。
$ node -v # vxx.xx.xのような形で表示されれば正解
$ npm -v # vx.x.xのような形で表示されれば正解
プロジェクトを作成する
プロジェクトを作成しましょう。ターミナルを開き、以下のコマンドを実行しましょう。
$ cd path/to/your/directory
$ npx create-react-app sample-atomic --template typescript
sample-atomicディレクトリが作成されていることを確認したら、以下のコマンドを実行して、アプリケーションを立ち上げましょう。
$ cd sample-atomic
$ npm start
Atomic Designを導入する
必要なフォルダを作成し、routingのためのライブラリをインストールします。
$ mkdir src/components
$ mkdir src/components/atoms
$ mkdir src/components/molecules
$ mkdir src/components/organisms
$ mkdir src/components/pages
$ touch src/components/pages/Home.tsx
$ npm install @types/react-router-dom
App.tsxを以下のように編集します。
import React from 'react';
import { BrowserRouter, Route } from 'react-router-dom';
import './App.css';
import Home from './components/pages/Home';
const App: React.FC = () => {
return (
<BrowserRouter>
<Route exact path='/' component={Home}></Route>
</BrowserRouter>
);
}
export default App;
material-uiをインストールしてからHome.tsxを編集します。
$ npm i @material-ui/core
import React, { useState } from "react";
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
const Home: React.FC = () => {
const [todos, setTodos] = useState<string[]>(["test1", "test2", "test3"]);
const [value, setvalue] = useState<string>("");
const onChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value;
setvalue(value);
};
const onSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault();
setTodos([...todos.concat(value)]);
};
return (
<div>
<header>
<h1>This is Header</h1>
</header>
<List>
{todos.map(todo => (
<ListItem key={todo}>{todo}</ListItem>
))}
</List>
<form onSubmit={onSubmit}>
<input type='text' onChange={onChange} />
<input type='submit' value='submit' />
</form>
</div>
);
};
export default Home;
以下のような画面が出てくれば正解です。
このアプリケーション内にある要素をAtomic Designに沿って分解していきます。上記をorganismsの粒度のコンポーネントに分けると、
- Header
- List
- Form
となり、その中でさらにmoleculesとatomsに分解していきます。結果的に、src/componentsは以下のようなディレクトリ構成になりました(templateは今回作っていません)。
├── components
│ ├── atoms
│ │ └── FormInput.tsx
│ ├── molecules
│ │ └── Form.tsx
│ ├── organisms
│ │ ├── FormWrapper.tsx
│ │ ├── Header.tsx
│ │ └── TodoList.tsx
└── └── pages
└── Home.tsx
import React, { useEffect, useState } from "react";
import Header from '../organisms/Header';
import TodoList from "../organisms/TodoList";
import FormWrapper from "../organisms/FormWrapper";
const Home: React.FC = () => {
const [todos, setTodos] = useState<string[]>(["test1", "test2", "test3"]);
const [value, setvalue] = useState<string>("");
const onChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value;
setvalue(value);
};
const onSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault();
setTodos([...todos.concat(value)]);
};
return (
<div>
<Header text='This is Header' />
<TodoList todos={todos} />
<FormWrapper onChange={onChange} onSubmit={onSubmit} />
</div>
);
};
export default Home;
import React from "react";
interface IProps {
text: string;
}
const Header: React.FC<IProps> = ({text}) => {
return (
<header>
<h1>{text}</h1>
</header>
);
};
export default Header;
import React from "react";
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
interface IProps {
todos: string[];
}
const TodoList: React.FC<IProps> = ({todos}) => {
return (
<List>
{todos.map(todo => (
<ListItem key={todo}>{todo}</ListItem>
))}
</List>
)
}
export default TodoList;
import React from "react";
import Form from "../molecules/Form"
import FormInput from "../atoms/FormInput"
interface IProps {
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
const FormWrapper: React.FC<IProps> = ({onSubmit, onChange}) => {
return (
<Form onSubmit={onSubmit}>
<FormInput onChange={onChange} />
<input type='submit' value='submit' />
</Form>
);
};
export default FormWrapper;
import React from "react";
interface IProps {
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
}
const Form: React.FC<IProps> = ({ children, onSubmit }) => {
return <form onSubmit={onSubmit}>{children}</form>;
};
export default Form;
import React from "react";
interface IProps {
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
const FormInput: React.FC<IProps> = ({ onChange }) => {
return <input type='text' onChange={onChange} />;
};
export default FormInput;
コンポーネント分解し終えた後に、Todoアプリが通常通り動けば成功です!
終わりに
今回のように非常に小さなアプリケーションでは、Atomic Designを採用するメリットは見えづらいですが、実際に実務で使う場合は先述のメリットを享受することができます。今回の記事で、Atomic Designという考え方と、大まかな雰囲気を知っていただければ幸いです。
Atomic DesignはReactと相性が良く、今後も使われていくデザインの概念だと思います。ReactやVueなどのコンポーネント指向のライブラリを使う場合は、是非一緒に採用してみてください。