Render-as-you-fetchってなに??
Render-as-you-fetchとは
まず、ReactのSuspenseは従来のデータフェッチのアプローチとは異なるアプローチをとっている。
それがRender-as-you-fetchというデータフェッチのアプローチ方法である。
このRender-as-you-fetchというアプローチを知るには、従来のデータフェッチのアプローチについて知る必要がるため、軽くまとめる。
従来のデータフェッチのアプローチにはRender-as-you-fetchを含め、以下のようなものがある。
-
Fetch-on-render→ 例:useEffect内でのフェッチ -
Fetch-then-render→ 例:Relayを使ったデータフェッチ(suspenseは使わない) -
Render-as-you-fetch→ 例:Suspenseでのデータフェッチ
個々の詳細の説明については別スクラップで。
Fetch-on-render
このデータフェッチのアプローチは昨今のReactでのデータフェッチではよく見るやつ。
useEffect内でデータフェッチを行う非同期関数を呼び出すみたいなタイプ。
サンプルコード
(※クラスコンポーネントの方もcomponentDidMountとか書いてあったけどめんどいから省略)
// in function component
useEffect(() => {
fetchSomething();
}, []);
このアプローチ方法がFetch-on-renderと呼ばれるのは、画面上にコンポーネントがレンダリングされてからデータフェッチが行われるからである。
(ここでいうレンダリングはブラウザレンダリング??????な気がする)
以下のようなProfilePageとProfileTimeLineという二つのコンポーネントを例に考えてみる。
function ProfilePage() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(u => setUser(u));
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline />
</>
);
}
function ProfileTimeline() {
const [posts, setPosts] = useState(null);
useEffect(() => {
fetchPosts().then(p => setPosts(p));
}, []);
if (posts === null) {
return <h2>Loading posts...</h2>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
上から順にデータフェッチが行われるため、Fetch-on-renderはウォーターフォール と呼ばれたりする。
(並列処理的にデータフェッチされるのではなく、シーケンスとなる。)
Fetch-then-render
Reactのデータフェッチで、ライブラリを使用することでFetch-on-renderのデータフェッチで問題視された「ウォーターフォール」を解決することもできる。(例えばRelayなど)
※Reactの公式では、Relayの知識がなくても理解できるようにあえてRelayを使用していなかった。
Fetch-on-renderでは、とあるデータフェッチが完了するまで次のデータフェッチが出来ずシーケンスになっていたことが問題になっていた。
そこで、複数のPromiseを受け取り、全てのPromiseがrejectされずにresolveされた場合に一つのPromiseを返すPromise.allを使用する。
要するにPromiseを使用することで、データ取得してからコンポーネントをrenderさせるようにすることができる。
/*
* fetchUserとfetchPostsは非同期Fetch関数
* (おそらくPromise<User>やPromise<Post[]>を返すと思われる)
* */
function fetchProfileData() {
return Promise.all([
fetchUser(),
fetchPosts()
]).then(([user, posts]) => {
return {user, posts};
})
}
「レンダリング前にfetchする」(Fetch-then-render)パターンでは必要なデータの取得を上位コンポーネントにまとめ、そこでPromise.allしてデータ取得ができてから下位のコンポーネントをレンダリングする。
→ Next.jsのgetInitialProps/getServerProps/getStaticPropsがイメージとして近そう
// なるはやでデータフェッチが実行される
const promise = fetchProfileData();
function ProfilePage() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
useEffect(() => {
promise.then(data => {
setUser(data.user);
setPosts(data.posts);
});
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline posts={posts} />
</>
);
}
// 子供コンポーネント側でデータフェッチすることはない
function ProfileTimeline({ posts }) {
if (posts === null) {
return <h2>Loading posts...</h2>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
Render-as-you-fetch
Render-as-you-fetchは必要なデータ取得を上位にまとめる意味ではRender-then-fetchと同じだが、Promise.allをしないで、個々のPromiseを取得・回すようなイメージ。
このデータフェッチパターンでは、Suspenseを使用することでデータフェッチ完了前にレンダリングを開始することができる。
【サンプルコード】
// この関数はPromiseじゃない?らしい
const resource = fetchProfileData();
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
);
}
function ProfileDetails() {
// この時点でuserのデータは取得できない可能性がある
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
// この時点でuserのデータは取得できない可能性がある
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
ただし複数のPromiseを取り回すケースでは、コードが複雑になりがちになる。
そこでSuspenseを使用することで、コンポーネントのデータ依存と優先度を宣言的に記述することができる。
Render-as-you-fetchパターンを実現するには、データの取得をSuspenseが置かれている位置よりも上に引き上げる必要がある。
実際どこにしたいのかと言うと、ページ遷移によって発生するデータ取得をuseTransitionで処理したいので、個々のページよりも上、ルートでやる必要がある。これを何とかして個々のコンポーネント単位に収めたいわけだが…
とりあえずページ単位でのデータ取得までの分割は比較的簡単にできて、これはNext.jsを参考にページ定義単位でgetInitialPropsを書くようにすることで実現できると思う。