🐙
役割駆動開発のすすめ[フロントエンド編]
役割に注目するコンポーネントの実装
あなたはhogeとfugaをサーバーから取得して、画面に表示するコンポーネントを実装しているとします。
つぎのうち、どちらのコンポーネントの書き方がよいと思いますか?
パターン1
const HogeFugaComponent = () => {
const [isLoadingHoge, setIsLoadingHoge] = useState(false);
const [hoge, setHoge] = useState(null);
const [isLoadingFuga, setIsLoadingFuga] = useState(false);
const [fuga, setFuga] = useState(null);
useEffect(() => {
fetchHoge()
.then(hoge => {
setHoge(hoge);
})
})
useEffect(() => {
fetchFuga()
.then(resFuga => {
setFuga(resFuga);
})
})
return <div>{fuga}{hoge}</div>
}
パターン2
const HogeFugaComponent = () => {
// hogeを表示する機能
const [isLoadingHoge, setIsLoadingHoge] = useState(false);
const [hoge, setHoge] = useState(null);
useEffect(() => {
fetchHoge()
.then(hoge => {
setHoge(hoge);
})
})
// fugaを表示する機能
const [isLoadingFuga, setIsLoadingFuga] = useState(false);
const [fuga, setFuga] = useState(null);
useEffect(() => {
fetchFuga()
.then(resFuga => {
setFuga(resFuga);
})
})
// 表示
return <div>{fuga}{hoge}</div>
}
Reactに馴染みのない方に説明すると、useState
というのはReactにおける状態管理関数です。返り値は配列なのですが、1つ目に実際の状態、2つ目にsetterが格納されております。そしてuseEffect
というのは、Reactコンポーネントにおいてサーバーとの通信など、副作用が生じるコードを書く場所になります。useEffect
はコンポーネントがマウントされると実行されます。
僕は、パターン2をおすすめします。なぜかというとパターン2のように関連するものを近くに集めたほうがモジュール化がしやすいからです。Reactにおけるモジュール化はコンポーネント化やカスタムフックなどで実現できます。
const HogeFugaComponent = () => {
// hogeを表示する機能
const [hoge, isLoadingHoge] = useHoge(); // カスタムフック。実装は省略
// fugaを表示する機能
const [fuga, isLoadingFuga] = useFuga(); // カスタムフック。実装は省略
// 表示
return <div>{fuga}{hoge}</div>
}]
パターン2のように書くために
パターン1もパターン2も、どちらも似たような機能を集めています。パターン1ではReactが提供するコンポーネントの機能単位で整理しました。パターン2ではコンポーネントが提供したい機能単位で整理しました。
パターン2のように書くにはどうしたらいいでしょうか?それは、コンポーネントの役割を明示的に書くことで実現できます。次のようにdocとして役割を箇条書きし、それをコメント分割でコード内に示してあげます。
/**
* 役割:
* - hogeを表示する機能
* - fugaを表示する機能
*/
const HogeFugaComponent = () => {
/* ------------------------ */
/* hogeを表示する機能 */
/* ------------------------ */
const [hoge, isLoadingHoge] = useHoge();
/* ------------------------ */
/* fugaを表示する機能 */
/* ------------------------ */
const [fuga, isLoadingFuga] = useFuga();
// 表示
return <div>{fuga}{hoge}</div>
}
わざわざこのように書く人はいないですよね。ここが役割をコメントとしてコードに明示するメリットです。
/**
* 役割:
* - 状態保持機能
* - 副作用機能
*/
const HogeFugaComponent = () => {
/* ------------------------ */
/* 状態保持機能 */
/* ------------------------ */
const [isLoadingHoge, setIsLoadingHoge] = useState(false);
const [hoge, setHoge] = useState(null);
const [isLoadingFuga, setIsLoadingFuga] = useState(false);
const [fuga, setFuga] = useState(null);
/* ------------------------ */
/* 副作用機能 */
/* ------------------------ */
useEffect(() => {
fetchHoge()
.then(hoge => {
setHoge(hoge);
})
})
useEffect(() => {
fetchFuga()
.then(resFuga => {
setFuga(resFuga);
})
})
// 表示
return <div>{fuga}{hoge}</div>
}
実際の例
/**
* 役割:
* - チャンネルを検索できる。
* - チャンネル一覧を表示する。 -> ChannelListへ委譲
* - チャンネルを選択すると、チャット画面に移動する。 -> ChannelListへ委譲
*/
export const ChannelIndex: React.FC = () => {
/* ------------------------ */
/* 共通 */
/* ------------------------ */
const history = useHistory();
const location = useLocation();
const params = queryString.parse(
location.search,
) as Partial<ChannelIndexPageQuery>;
/* ------------------------ */
/* チャンネル一覧表示 */
/* ------------------------ */
const [isLoading, setIsLoading] = useState(false);
const [channels, setChannels] = useState<Channel[]>([]);
const [total, setTotal] = useState(0);
// URLが変更されると(つまり検索条件が変更すると)、ページの最初までスクロール
useEffect(() => {
window.scrollTo(0, 0);
}, [location]);
/* ------------------------ */
/* チャンネル検索 */
/* ------------------------ */
// NOTE: 検索は基本的に
// 1.urlにpush
// 2.urlの変更を検知してurlからparamsを取得しfetch
// という流れで行われる。
// この方針をとった理由としては、検索条件を内部stateではなくurlに保持することによって、リンクでの共有が可能になるから。
const PAGE_UNIT = 20;
const currentPage = Number.parseInt(params.currentPage || '1', 10);
const handleSearchPage = (page: number) => {
history.push({
search: queryString.stringify({
...params,
currentPage: page.toString(),
}),
});
};
const handleSearchSubmit = (text: string) => {
history.push({
search: queryString.stringify({
...params,
keyword: text,
}),
});
};
useEffect(() => {
const offset = (currentPage - 1) * PAGE_UNIT;
const limit = PAGE_UNIT;
setIsLoading(true);
ChannelRequest.search({
keyword: params.keyword,
offset: offset.toString(),
limit: limit.toString(),
})
.then((res) => {
if (!res.isSuccess) {
notify('error', res.error);
return;
}
setTotal(res.header.total);
setChannels(res.body);
})
.finally(() => {
setIsLoading(false);
});
}, [
// NOTE: paramsで指定するとループするため、明示的に依存プロパティを指定する。
currentPage,
params.keyword,
params.limit,
params.offset,
]);
return (
<div className={classes.Cover}>
<Container className={classes.Container}>
<Search className="mb-3 h-2" onSubmit={handleSearchSubmit} />
<ChannelList
channels={channels}
isLoading={isLoading}
currentPage={currentPage}
pageUnit={PAGE_UNIT}
total={total}
handlePage={handleSearchPage}
searchKeyword={params.keyword || ''}
/>
</Container>
</div>
);
};
テストでも、対応する役割をチェックすればいいと思います。
describe('<ChannelIndex />', () => {
describe('チャンネルを検索できる。', () => {
test.todo('チャンネルをキーワードで検索できる');
test.todo('次のページのチャンネルを検索できる');
test.todo('前のページのチャンネルを検索できる');
});
});
おわり
以下のこと考えているときに、この書き方をひらめきました。
結局TDDによる「意図によるプログラミング」と同じ効果になりそうです。
Discussion