本記事は React best practices and patterns to reduce code を提供元の事前許可を得たうえで翻訳したものです。
元の記事に従いタイトルに「ベストプラクティス」と含んでいますが、実際にはベストプラクティスは規模や状況によって大きく異なります。
チームの状況にあわせて参考にしていただければと思います。
=====
これは全3パート中の第1パートとなる記事です。
私は数年に渡っていくつかのプロジェクトで、React.jsを使った取り組みに参加してきました。様々なプロジェクトに取り組む中でいくつかの共通するパターンを見出したため本ブログでご紹介いたします。それではいきましょう。
1. reduxのactionsとdispatcherのためにカスタムフックを作成する
私はreduxを使うことを好んではいませんが、いくつかのプロジェクトではreduxを使っています。それらの私が参加したほぼ全てのプロジェクトでは redux-thunk を利用していました。そこでは冗長なコード(ボイラープレート)がとてもよく存在していることがわかりました。
※状態管理については別の記事も作成しています。
3 steps to create custom state management library
const useUser = () => {
const dispatch = useDispatch();
const state = useSelector(); // get auth info or something
const fetchUser = (id) => {
return fetch(`/api/user/${id}`).then((res) => res.json())
.then((user) => dispatch({type: "FETCH_USER",payload:user}));
};
const fetchUsers = () => {
return fetch('/api/users').then((res) => res.json())
.then((user) => dispatch({type:"FETCH_USERS",payload: user}));
};
return { fetchUser, fetchUsers };
}
コンポーネント側(利用側)の記述
const { fetchUser } = useUser();
useEffect(() => fetchUser(1), [])
注釈:このように全てのreduxアクションに対して複数の関数を作成する必要はありません。なお、useSelector
を使えばreduxから様々な情報を取得できます。
2. reducer内部で処理を分ける代わりにオブジェクトを使用する
まず、switch文の代わりにオブジェクトリテラルを使用することができます。オブジェクトリテラルはより読みやすくメンテナンスも簡単です。
const actionMap = {
INCREMENT:(state, act) => ({...state, count: state.count + 1 }),
DECREMENT: (state, act) => ({...state, count: state.count - 1 }),
}
const reducer = (state, action) => {
const handler = actionMap[action.type];
return handler ? handler(state, action) : state;
};
注釈:再評価を避けるために必ずdispatchの外側でオブジェクトの定義を行なってください。map(object)の検索がO(1)であるのに対し、スイッチ文はO(log n)となってしまいます。
3. REST呼び出しのhookを作成する
ブラウザのfetch APIを利用してhookを作成することで重複コードを避けられます。APIから取得したデータで状態を更新してレンダリングします。
const useFetch = (input, { auto, ...init }) => {
const [result, setResult] = useState([null, null, true]);
const fetcher = useCallback(
(query, config) =>
fetch(query, config)
.then((res) => res.json())
.then((data) => setResult([null, data, false]))
.catch((err) => setResult([err, null, false])),
[input, init]
);
useEffect(() => {
if (auto) fetcher(input, init);
}, []); // if you want to fetch data only once, do this.
return [...result, fetcher];
//fetcher(refetch) function or can be used for post api call
};
コンポーネント側(利用側)の記述
const Users = () => {
const [err, users, loading, refetch] = useFetch(`/api/users`, {auto:true});
const onClick = () => refetch(...);
return (
<div>
{users.map((user) => <User key={user.id} user={user} />)}
</div>
);
}
注釈:react-queryやuseSWRといったライブラリと似ていますが、どちらのライブラリもより多くの機能を提供しています。これらのライブラリを使うこともできますが、プロジェクトに制約がある場合は上述の方法でREST API実装を用意しておくと余分なコードを省くことができます。
4. コードの分割
React.lazyは必要な時にだけコンポーネントをロードすることができる強力なツールです。React.lazy機能を使うと、必要なタイミングでのみ動的にインポートしてレンダリングすることができます。
導入の手始めとしてはルーティングに着目するのがよいでしょう。従来の方法ではレンダリングする前にコンポーネントをロードしなければなりませんがこれは好ましくありません。コンポーネントを表示しないにもかかわらず、すべてのコンポーネントをロードするために余計な時間がかかってしまうためです。
繰り返しになりますがReact.lazyを使うことでコンポーネントを非同期で読み込むことができます。つまり、以下のコードであれは最初のページ(Home)にいるときに最初のコンポーネントをロードし、2番目のページ(About)にいるときに2番目のコンポーネントをロードすることができます。こうすることで、無駄なコンポーネントの読み込みを避けることができます。
const Home = React.lazy(() => import('./Home'));
const About = React.lazy(() => import('./About'));
function MyComponent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Route path="/home" component={Home} />
<Route path="/about" component={About} />
</Suspense>
);
}
注釈:この例はとてもシンプルでしたが実際のプロジェクトでは100個のルーティングが存在している場合もあるでしょう。そうした場合には絶大なパフォーマンス効果を得られます。
参照:Code-Splitting - React (reactjs.org)
--
今後もWebフロントエンドにまつわる最新情報を発信予定です。
興味のある方はぜひ 公式Twitter をフォローしてください。