「ぼくがかんがえた」と言っていますが、実際には本を読んだりインターンで実際にAtomic Designが導入されているプロダクトに触れ、それらを総合し一番良さそうだと思った設計について書いていきます。正直これが正解かどうかはわかりません。
余談ですが僕は化学畑出身のフロントエンドエンジニアなので、僕よりAtomic Designが似合うエンジニアはそういないと思います。w
Atomic Designとは
コンポーネントの設計方法のひとつでAtom(原子)・Molecule(分子)・Organism(有機体)・Template(テンプレート)・Page(アプリケーション)の五つの層にコンポーネントを分類します。
ただ、明確な分類基準があるわけではないので分類基準が不透明になりがちです。
この分類基準の一つとして有名なAtomic Designの本があります。詳細はこの本の読んだ内容をまとめた以下の僕の記事をご覧ください。
しかし、この本の設計でやるとコンポーネントのネストが深くなりバケツリレーが悲惨なことになりそうだなというのが僕の正直な感想です。
そこで、もう少し綺麗になりそうな設計を自分なりに考えてみました。
各層の役割
ここからは僕が良いと思う分類についてまとめていきます。具体例については後述します。
Atom
この層はおそらくAtomic Designを使う以上は役割が変わらないと思います。それ以上分解できないUIパーツをAtomとします。
Molecule
複数のAtomやMoleculeを組み合わせて表示だけの責務を持つコンポーネントをMoleculeとします。表示する値はpropsから渡されたものだけで、reduxについては全く知らない状態です。
Organism
複数のAtomやMoleculeなどを組み合わせたアプリケーションの状態やロジックについて関心を持つコンポーネントです。具体的には、reduxからstateやdispatchなどをconnectし、かつ表示にも責任をもちます。
そのため、containerとpresentationに分ける必要があり、mergeProps()
によりロジックを全てcontainerに持つのが理想的です。(後述)
Template
アプリケーションのレイアウトにのみ責任を持ちます。理想的にはreduxについて知らない方がいいと思うのですが、ページ固有のロジックや状態などはここで管理した方が冗長なOrganismが増えずに済んでいいのかもしれません。
Page
storeの接続などを行います。正直TemplateとPageを分ける意味があまりないので、この層ではアプリケーション全体について責任を持ちSPAならroutingについても責任を持つといい気がします。
つまり、
Template → Page
Page → App
として管理するということです。
reduxとreactの責務
ここは今までよく悩んだところだったのですが僕の解としては、reduxは状態に関してのみ責任を持つべきでロジックはcontainerで持てばいいというものです。例外的に非同期処理はreduxのmiddlewareで持つべきですが、それ以外は基本的にcontainerで持った方が綺麗になる気がしています。
mergeProps()
を使うとコールバックすらcontainerに書けるのでpresentationは表示だけに責任を持ったコンポーネントにすることができます。
もちろん、localのstateを持ちたいことは出てくるのでそのときは臨機応変にstateをコンポーネント内で持てばいいと思います。
ただ現実的には冗長さとのトレードオフになってしまうので全てこれに従うのは難しそうですが。。
少なくともどこに何を書けばいいかの指針があると書くのも読むのも楽になるのではないでしょうか?
TodoアプリをAtomic Designで作ってみる
Atom
Button
import React from 'react';
import styles from './style.css'
const buttonFactory = type => ({ children, className, ...props }) => {
return <button className={[styles.button, styles[type], className].join(" ")} { ...props }>{ children }</button>
}
export const NormalButton = buttonFactory("")
export const PrimaryButton = buttonFactory("primary")
export const WarningButton = buttonFactory("warning")
Factory関数を使って抽象化して複数のパターンを作っていますが、それ以外特に特殊なことはしていません。
Input
import React from "react";
import styles from "./style.css";
const inputFactory = type => props => {
return <input className={styles[type]} type={type} {...props} />;
};
export const TextInput = inputFactory("text");
export const CheckBox = inputFactory("checkbox");
こちらもbuttonと同じような構成です。
Molecule
ListItem
import React from "react";
import { CheckBox } from "../../atoms/Input/index";
import { WarningButton } from "../../atoms/Button/index";
import styles from "./style.css";
const ListItem = ({ id, children, done, handleChange, handleDelete }) => {
return (
<li className={styles.items}>
<CheckBox checked={done} onChange={handleChange} />
<span>{children}</span>
<WarningButton onClick={() => handleDelete(id)}>Delete</WarningButton>
</li>
);
};
export default ListItem;
複数のAtomから構成されますが表示にしか責務を持ちません。そのためMoleculeに分類しました。
Organism
Form
import React from "react";
import { TextInput } from "../../atoms/Input";
import { PrimaryButton } from "../../atoms/Button";
const Form = ({ value, handleSubmit, handleChange }) => {
return (
<div>
<TextInput value={value} onChange={handleChange} />
<PrimaryButton onClick={handleSubmit}>Add</PrimaryButton>
</div>
);
};
export default Form;
import Form from "./presentation";
import { addTodo, changeValue } from "../../../redux/actions";
import { connect } from "react-redux";
const mapStateToProps = props => props;
const mapDispatchToProps = dispatch => ({
addTodo: item => dispatch(addTodo(item)),
changeValue: value => dispatch(changeValue(value))
});
const mergeProps = (stateProps, dispatchProps) => {
const { form, todo = [] } = stateProps;
const { addTodo, changeValue } = dispatchProps;
return {
handleChange: e => changeValue(e.target.value),
handleSubmit: () => {
const id = todo.length + 1;
const _todo = { id, content: form, done: false };
addTodo(_todo);
changeValue("");
},
value: form
};
};
const FormContainer = connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(Form);
export default FormContainer;
メリットのひとつとしてコンポーネントが無駄な情報について知ることがなくなります。stateの影響範囲を小さくすることで変更に強くなります。
ただ、どこまでやるか問題も同時に起きてきて、例えば表示に関するデータの整形までしてしまうと項目が増えてきたときにporpsが爆増して管理が返って煩雑さがましてしまいます。
さらにコンポーネントを分割するのか諦めてpresentationに書くのかは、実装コストと設計のトレードオフになりそうです。
List
import React from "react";
import ListItem from "../../molecules/ListItem/index";
import styles from "./style.css";
const List = ({ items = [], ...options }) => {
return (
<ul className={styles.list}>
{items.map((item = {}, index) => (
<ListItem key={item.id || index} {...options} {...item}>
{item.content}
</ListItem>
))}
</ul>
);
};
export default List;
import List from "./presentation";
import { connect } from "react-redux";
import { deleteTodo, putTodo } from "../../../redux/actions";
const mapStateToProps = ({ todo }) => ({ todo });
const mapDispatchToProps = dispatch => ({
deleteTodo: todos => dispatch(deleteTodo(todos)),
putTodo: todos => dispatch(putTodo(todos))
});
const mergeProps = (stateProps, dispatchProps) => {
const { todo } = stateProps;
const { deleteTodo, putTodo } = dispatchProps;
return {
handleDelete: id => {
const _todo = todo.filter(t => t.id !== id);
deleteTodo(_todo);
},
handleChange: id => {
const _todo = todos.map(t => (t.id === id ? { ...t, done: !t.done } : t));
putTodo(_todo);
},
items: todo
};
};
const ListContainer = connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(List);
export default ListContainer;
todoの更新や削除に関してはcontainerで記述しています。
Template
AppTemplate
import React, { Fragment } from "react";
import ListContainer from "../../organisms/List/container";
import FormContainer from "../../organisms/Form/container";
const AppTemplate = () => {
return (
<Fragment>
<FormContainer />
<ListContainer />
</Fragment>
);
};
export default AppTemplate;
何をrenderするかのみの責任を持っています。
Page
App
import React from "react";
import { Provider } from "react-redux";
import { createStore } from "redux";
import reducers from "../../../redux/reducers";
import AppTemplate from "../../templates/AppTemplate/index";
const store = createStore(reducers);
const Application = () => {
return (
<Provider store={store}>
<AppTemplate />
</Provider>
);
};
export default Application;
storeの情報だけを渡しています。
まとめ
Todoごときには大げさすぎましたが、大規模になるとけっこう効果を発揮してくれそうです。(例えば、同じ見た目でロジックが若干違うTodoを実装しないといけないとき?とかw)
正直コードを書くのはとても大変ですが、大規模なプロダクトのコードを読んだときはその読みやすさに感動しました。
コードは以下のリポジトリにあげています。(cssは適当)ちなみにreduxはducksパターンで実装しています。