Reactでプロジェクトをつくる際、コンポーネント間で共通部分を一つにまとめることを意識しています。
表示部分に関してはできている(つもり)のですが、処理部分についてはレビューでも何回か指摘されるなど、あまりできていませんでした。
カスタムフックを利用すればいいことはわかっていたのですが、そもそもの作り方や使い方を理解できていなかったことが原因です。
ただ、カスタムフックをいくつかつくってみると作成にはコツがあることがわかりましたので、記事にまとめることにしました。
処理の共通化について悩んでいる方の参考になればと思います。
#カスタムフックとは
カスタムフック(Custom Hooks)は、コンポーネント間で共通な処理(ロジック)を1つにまとめた再利用可能な関数です。
例えば以下のように、AppとAnalyticsという2つコンポーネントで"動画の取得"や"動画の選択"といった処理が共通である場合、各処理のカスタムフックを作成することでどちらのコンポーネントでも同じ処理を使えるようになります。
#カスタムフック作成のコツ
作成のコツとして”作成時に意識すること”と”作成のプロセス”についてまとめました。
##作成時に意識すること
- カスタムフックのコードはフックを利用するコンポーネントとは分離する
- カスタムフックの中ではReact Hook(use~)を少なくとも1つ以上使う
- 1つのカスタムフックは1つの目的だけをもつ
特に最後の"1つのカスタムフックは1つの目的だけをもつ"については、SOLIDの単一責任の原則に近い考え方になります。
一人のアクターが使うようにカスタムフックが設計されていないと、気づかないうちに別のアクターによって中身が修正されてしまったり、同じフックを2人以上のアクターが修正してしまうことでコンフリクトが起こってしまう可能性がでてきてしまいます。
##作成のプロセス
- 処理部分を一行ずつ確認し目的ごとに分別する
- Inputを抽出する
- Outputを抽出する
- Inputを引数、Outputを戻り値としてコードをカスタムフックに抽出する
##カスタムフックの作成例
YouTubeのAPIから動画を5件取得して、選択した動画をメインに表示するような画面を考えます。
ちなみにデフォルト表示は"YAMADA KATSUMI"の検索結果です。
動画の取得処理に関するカスタムフックuseVideos
を作成していきます。
もともとのコードは以下のようになっています。
const App = () => {
const [videos, setVideos] = useState([]);
const [selectedVideo, setSelectedVideo] = useState(null);
useEffect(() => {
onTermSubmit('YAMADA KATSUMI');
}, []);
const onTermSubmit = async (term) => {
const response = await youtube.get('/search', {
params: {
q: term,
},
});
setVideos(response.data.items);
setSelectedVideo(response.data.items[0]);
};
return (
<div className="ui container">
<SearchBar onFormSubmit={onTermSubmit} />
<div className="ui grid">
<div className="ui row">
<div className="eleven wide column">
<VideoDetail video={selectedVideo} />
</div>
<div className="five wide column">
<VideoList onVideoSelect={setSelectedVideo} videos={videos} />
</div>
</div>
</div>
</div>
);
};
###処理部分を一行ずつ確認し目的ごとに分別する
動画の取得処理を"Video"、動画の選択処理を"Selection"として該当コードを分別します。
###Inputを抽出する
カスタムフックを複数のコンポーネントで使えるように、"Video"の処理でInputになる箇所を抽出します。
今回のケースだと"YAMADA KATSUMI"の部分がInputにあたります。
これでカスタムフックを使用するコンポーネントごとにデフォルトの検索条件を変えることができるようになります。
###Outputを抽出する
コンポーネントで使用する箇所をOutputとして抽出します。
今回のケースでは、videos
とonTermSubmit
が該当します。
###Inputを引数、Outputを戻り値としてコードをカスタムフックに抽出する
抽出したInputとOutputをもとに、カスタムフックuseVideos
を作成します。
Inputにあたる部分をdefaultSearchTerm
とし、Outputにあたる部分をreturn [videos, search]
としています。
import { useState, useEffect } from 'react';
import youtube from '../apis/youtube';
const useVideos = (defaultSearchTerm) => {
const [videos, setVideos] = useState([]);
useEffect(() => {
search(defaultSearchTerm);
}, [defaultSearchTerm]);
const search = async (term) => {
const response = await youtube.get('/search', {
params: {
q: term,
},
});
setVideos(response.data.items);
};
return [videos, search];
};
export default useVideos;
作成したカスタムフックuseVideos
をApp.js
でconst [videos, search] = useVideos('YAMADA KATSUMI')
のように読み込みます。
処理が一つにまとまったことでコードも読みやすくなりました。
const App = () => {
const [selectedVideo, setSelectedVideo] = useState(null);
const [videos, search] = useVideos('YAMADA KATSUMI');
useEffect(() => {
setSelectedVideo(videos[0]);
}, [videos]);
return (
<div className="ui container">
<SearchBar onFormSubmit={search} />
<div className="ui grid">
<div className="ui row">
<div className="eleven wide column">
<VideoDetail video={selectedVideo} />
</div>
<div className="five wide column">
<VideoList onVideoSelect={setSelectedVideo} videos={videos} />
</div>
</div>
</div>
</div>
);
};
#参考資料