React Suspenseで不要な描画処理をなくす
React Freeze が、Suspenseのユニークな利用方法を提案をしていたので記事にまとめました。
おさらい: React Suspense とは
React Suspense は React 16.6から実験的に追加されたコンポーネントで、これによって、ロード状態であることを宣言的に指定できるようになります。
Suspenseは、データの読み出しが準備できているかどうかをReactに伝える仕組みであるとドキュメントには書かれており、データ取得の際に使うことが主なユースケースと考えられます。
const ProfilePage = React.lazy(() => import('./ProfilePage')); // Lazy-loaded
// Show a spinner while the profile is loading
<Suspense fallback={<Spinner />}>
<ProfilePage />
</Suspense>
データが呼び出し中であるかどうかは、Suspenseの子コンポーネント内で promise を throw することで、React に伝えることができます。
function ProfileDetails() {
const user = resource.user.read(); // throw promise here
return <h1>{user.name}</h1>;
}
この仕様自体、それだけで記事になるほど面白いです。(ここでは割愛します。)
React Freeze
React Freezeは、そんなSuspenseを利用したライブラリです。
元々React側は、データ取得の際に使うことが主なユースケースと言っていたのに対して、このライブラリの目的は、ある瞬間にユーザーに表示されていないアプリの部分について、不要な再レンダリングを避けることとなっています。
どうやって実現しているのでしょうか?
実は、ライブラリの実装もシンプルで興味深いです。
// ref: https://github.com/software-mansion-labs/react-freeze/blob/main/src/index.tsx
function Suspender({
freeze,
children,
}: {
freeze: boolean;
children: React.ReactNode;
}) {
const promiseCache = useRef<StorageRef>({}).current;
if (freeze && !promiseCache.promise) {
promiseCache.promise = new Promise((resolve) => {
promiseCache.resolve = resolve;
});
throw promiseCache.promise;
} else if (freeze) {
throw promiseCache.promise;
} else if (promiseCache.promise) {
promiseCache.resolve!();
promiseCache.promise = undefined;
}
return <Fragment>{children}</Fragment>;
}
export function Freeze({ freeze, children, placeholder = null }: Props) {
return (
<Suspense fallback={placeholder}>
<Suspender freeze={freeze}>{children}</Suspender>
</Suspense>
);
}
注目は、Suspenderコンポーネントにある、promiseCacheの実装部分で、freeze
propsがtrueである間(freeze中)、空のpromiseをthrowし続けます。そして、Suspenderがpromiseをthrowし続けている間、Freezeコンポーネント内のSuspenseによってそれを監視する仕組みになっています。
この実装の面白いところは、freeze中はSuspenseにより子コンポーネントが再描画せれず、代わりにSuspenseのfallbackを描画し、不要な描画処理をスキップしていることです。
また、Suspenseによって置き換えられた子コンポーネントは、アンマウントされないという性質があり、これによって、状態(ここでいう状態はstateではなく、スクロールの位置や、input、ロードされた画像など)を保持したまま再描画を抑制することが可能になります。
このライブラリが主に想定しているユースケースは、アプリのスタック型のナビゲーションであり、このSuspenseの性質を利用することでStackの一番上にあるコンポーネント以外をfreezeすることで、効率良く画面の状態をロックすることができます。
考察
React Freezeの、アンマウントせずに画面をフリーズされる機能によって、Native/Webでの画面の最適化の方法の一つとしてのSuspenseを利用を考えられそうです。実装自体がシンプルなので、色々と応用ができそうだと感じました。
スタックビューがメインのユースケースとして想定されていますが、Webでの利用についても少し言及されており、例えばモーダルなど画面を覆うようなコンポーネントで利用できそうです。
function App() {
const [showModal, setShowModal] = useState(false)
return (
<Fragment>
<Freeze freeze={showModal}>
<AppPage onOpenModal={() => setShowModal(true)} />
</Freeze>
<Freeze freeze={!showModal}>
<AppModal open={showModal} />
</Freeze>
</Fragment>
);
}
また、React Freezeによって、データの取得時の状態以外の用途でもSuspenseが使える可能性が示唆されたとも言えます。
データの読み出しが準備できているかどうかをReactに伝える仕組みであるというのが当初のSuspenseを提供する目的だと思っていましたが、それだけでなく、コンポーネントが描画可能な状態であることを伝える仕組みだと、より一般化して捉えられると思いました。
もしかしたら、React Suspenseの可能性はもっと大きいのかもしれないですね。
Discussion