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
を書くようにすることで実現できると思う。