こんにちは。梅津です。
前回書いたこちら↓の記事で Recoil を利用しているという話をしました。
今回は業務で Recoil を使っている中で「こうすれば再レンダリングを抑えられるなー」ということがわかってきたので、その紹介です。
作るものと利用するライブラリ
今回は次のようなログインフォームを例にサンプルコードを書いていきます。
サンプルコードで利用するライブラリとそのバージョンは次のとおりです。
- React 18.2.0
- Recoil 0.7.5
- MUI 5.10.4
また、コードの簡略化のためカスタムフックは作らない方針でいきます。
再レンダリングを抑えるためのポイント
ポイントになるのは次の2点です。
- Atom を小さく分割し、枝葉のコンポーネントで購読する
- イベントハンドリング時に Atom の値が欲しいなら
useRecoilCallback
を使う
ポイント1: Atom を小さく分割し、枝葉のコンポーネントで購読する
Recoil では useRecoilState
や useRecoilValue
を使って Atom の値を購読することができます。
Atom の更新がおこなわれると、その Atom を購読しているコンポーネントはすべて再レンダリングの対象となります。
その性質上、いくつものプロパティを持つ大きな Atom を複数のコンポーネントが購読してしまうと、再レンダリングされる対象が多くなってしまいます。
これは大きな Atom を Selector で分割していったとしても変わりません。(number や string といったプリミティブな値にまで分割すればメモ化されるようですが)
次に Atom は小さく分割したものの、コンポーネントツリーの親のほうで購読した場合はどうでしょう。
再レンダリング対象は親コンポーネントになるわけですから、条件によっては子のコンポーネントも再レンダリングされていくことになります。
再レンダリングを抑えるのに一番効果があるのは、Atom を小さく分割して、コンポーネントツリーの枝葉のほうのコンポーネントで購読することです。
関係するものを最小限にすることで再レンダリングを抑えることができます。
具体的には次のようなコードです。
// globalState/login.ts import { atom, DefaultValue, selector } from "recoil" interface EmailAtom { emailInputValue: string } export const emailAtom = atom<EmailAtom>({ key: "emailAtom", default: { emailInputValue: "", }, }) interface PasswordAtom { passwordInputValue: string } export const passwordAtom = atom<PasswordAtom>({ key: "passwordAtom", default: { passwordInputValue: "", }, })
// EmailInput.tsx import React, { useCallback } from "react" import { TextField } from "@mui/material" import { useRecoilState } from "recoil" import { emailAtom } from "../globalState/login" export function EmailInput(): JSX.Element { // Email用のAtomへ依存 const [{emailInputValue}, setState] = useRecoilState(emailAtom) const handleChange = useCallback( (event: React.ChangeEvent<HTMLInputElement>) => { setState((currVal) => { return { ...currVal, emailInputValue: event.target.value, } }) }, [] ) return ( <TextField label={"email"} value={emailInputValue} onChange={handleChange} /> ) }
// PasswordInput.tsx import React, { useCallback } from "react" import { TextField } from "@mui/material" import { useRecoilState } from "recoil" import { passwordAtom } from "../globalState/login" export function PasswordInput(): JSX.Element { // Password用のAtomへ依存 const [{passwordInputValue}, setState] = useRecoilState(passwordAtom) const handleChange = useCallback( (event: React.ChangeEvent<HTMLInputElement>) => { setState((currVal) => { return { ...currVal, passwordInputValue: event.target.value, } }) }, [] ) return ( <TextField label={"password"} type={"password"} value={passwordInputValue} onChange={handleChange} /> ) }
不要な再レンダリングを抑えるために状態を細かく分割していくというのは Context でも出てくる話です。
グローバルな状態を管理するためのツールが違っても、やることは意外と変わらないんだなと感じました。
ポイント2: イベントハンドリング時に Atom の値が欲しいなら useRecoilCallback
を使う
ユーザーのアクションに合わせて Atom に保存した値をサーバーへ送信したいということがよくあります。
例えばユーザーがログインボタンを押下したときのログイン処理などです。
このとき useRecoilState
などを使って Atom の値を取得してしまうと、ポイント1で述べたとおり再レンダリングの対象となってしまいます。
再レンダリングは抑えつつイベントハンドリング時に Atom の値を取得したいときは useRecoilCallback
を使います。
// LoginForm.tsx import React from "react" import { Button, Stack, Typography } from "@mui/material" import { EmailInput } from "./EmailInput" import { PasswordInput } from "./PasswordInput" import { useRecoilCallback } from "recoil" import { emailAtom, passwordAtom } from "../globalState/login" import { login } from "../api/login" export function LoginForm(): JSX.Element { const handleSubmit = useRecoilCallback( ({snapshot}) => async (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault() // snapshot から Atom の値を取得できる const {emailInputValue} = await snapshot.getPromise(emailAtom) const {passwordInputValue} = await snapshot.getPromise(passwordAtom) await login(emailInputValue, passwordInputValue) } ) return ( <form onSubmit={handleSubmit}> <Stack spacing={2}> <Typography>ログイン情報を入力してください</Typography> <EmailInput/> <PasswordInput/> <Button type={"submit"} variant={"contained"}> ログイン </Button> </Stack> </form> ) }
useRecoilCallback
は再レンダリングを抑える以外にも使いみちがあるようです。
詳しくは 公式ドキュメント に記載がありますので、一度目を通してみるのをおすすめします。
おわりに
いかがだったでしょうか。
Recoil 自体は Atom の粒度について言及していないので、どういう方針で実装するか迷う時期がありました。
ちょっとした実験をしてみた結果、 Atom を小さくしたほうが再レンダリングを抑えられることがわかってからは、なるべく Atom を小さくする方針で実装しています。
ところで私が現在携わっている社内プロダクトでは、今回のようにフォームの作成に Recoil を使ってきました。
なのですが、「そもそもフォームを作るならそれに特化したライブラリに移行したほうが保守やパフォーマンスの観点から良いのでは?」という流れになっており、react-hook-form などへの移行を考えています。
今後のフォーム作りがどう変わっていくのか。次の機会に紹介できたらと思います。
それでは、また。