🦔

VueのSuspenseについてのメモ

2024/02/13に公開

Vue3 でいまだ experimental な Suspense を触って感じたことのメモ。
React で同じように書いたら挙動違うだろうなと思って試してみたり。Suspense の違いというより、再描画の仕組みの違いによるもの?
React はだいぶ前にチュートリアルを触っただけなのと、Suspense についても前に少し調べたことがある程度なので間違いがあるかも。
Vue は 3.4.15
React は 18(Next.js 14.1.0)

結論

親コンポーネントから非同期コンポーネントへ渡す props を変更した場合、Vue はそれだけでは fallback コンテンツが再表示されず、React はそれだけで fallback コンテンツが再表示される。
以下のサンプルコードでは、Vue は初期表示のときにしか"Loading..."が表示されないが、React の場合は"change prop1"ボタンをクリックするたびに"Loading..."が表示される。

  • Vue

親コンポーネント

<script setup lang="ts">
import { ref } from "vue";
import AsyncSample from "./AsyncSample.vue";

const prop1 = ref("prop1");

const onChangeProp1Click = () => {
  prop1.value = prop1.value + "a";
};
</script>

<template>
  <div>SuspenseSample</div>
  <div>
    <button type="button" @click="onChangeProp1Click">change prop1</button>
  </div>
  <Suspense>
    <AsyncSample :prop1="prop1" />
    <template #fallback> Loading... </template>
  </Suspense>
</template>

非同期処理を行う子コンポーネント

<script setup lang="ts">
const props = defineProps<{ prop1: string }>();
const asyncFunction = async () => {
  return new Promise<void>((resolve) => {
    setTimeout(() => {
      console.log("asyncFunction");
      return resolve();
    }, 3000);
  });
};
await asyncFunction();
</script>

<template>
  <div>AsyncSample</div>
  <div>prop1 : {{ props.prop1 }}</div>
</template>

  • React
"use client";

import { Suspense, useState } from "react";

async function AsyncTest({ prop1 }: { prop1: string }) {
  const asyncFunction = async () => {
    return new Promise<void>((resolve) => {
      setTimeout(() => {
        console.log("asyncFunction");
        return resolve();
      }, 3000);
    });
  };
  await asyncFunction();

  return (
    <>
      <div>AsyncTest</div>
      <div>prop1 : {prop1}</div>
    </>
  );
}

export default function SuspenseSampleMain() {
  const [prop1, setProp1] = useState("prop1");
  const onChangeProp1Click = () => {
    setProp1(prop1 + "a");
  };

  return (
    <>
      <div>SuspenseSampleMain</div>
      <div>
        <button type="button" onClick={() => onChangeProp1Click()}>
          change prop1
        </button>
      </div>
      <Suspense fallback={<div>Loading...</div>}>
        <AsyncTest prop1={prop1} />
      </Suspense>
    </>
  );
}

(コンソール開くと、どうやら 3 回くらい再描画されているっぽい。よくわかってない。)

解説

Vue の場合、Suspense 内の一番親となるノードが再描画されない限り、fallback コンテンツが表示されることはない。

Once in a resolved state, <Suspense> will only revert to a pending state if the root node of the #default slot is replaced. New async dependencies nested deeper in the tree will not cause the <Suspense> to revert to a pending state.

https://vuejs.org/guide/built-ins/suspense.html#loading-state

setup 直下で await している場合を考えると、まあそうだろうなという感じ。サンプルコードでいう ↓ の部分は、props が変更されただけでは再実行されないため、 pending になることはない。fallback が表示されることもない。

await asyncFunction();

親で無理やり v-if で表示状態を管理すれば、再表示される。
でもこれだと親で Promise の状態に相当するものを管理することになるので、Suspense を使う意味がない。

<Suspense>
  <!-- isLoadingを一度falseにしてからtrueに変更するとfallbackコンテンツが表示される。 -->
  <AsyncTest v-if="isLoading" :prop1="prop1" />
  <template #fallback>Loading...</template>
</Suspense>

React の場合は、コンポーネント自身または祖先のいずれかの state が更新されると再描画されるため、子コンポーネントの非同期処理も再実行される。なので 再度 pending になって fallback が表示されるんだろうなという感じ。普段 React を使わないのでちゃんとはわかっていない。

コンポーネントがレンダーされる理由には 2 つあります。

  1. コンポーネントの初回レンダー。
  2. コンポーネント(またはその祖先のいずれか)の state の更新。

https://ja.react.dev/learn/render-and-commit#step-1-trigger-a-render

所感

Vue の Suspense は、現状だと初期表示のときくらいしか使い道がなさそう?
一番想像つくのは、router-view の遷移先が top-level await していたり非同期コンポーネントだったりするとき。
あとは SSR とか。例えば Nuxt だとデフォルトで Suspense が使われている。

Nuxt uses Vue’s <Suspense> component under the hood to prevent navigation before every async data is available to the view. The data fetching composables can help you leverage this feature and use what suits best on a per-calls basis.

https://nuxt.com/docs/getting-started/data-fetching#suspense
experimental だからここから仕様が変わる可能性はあるけど、Vue の再描画の仕組み的に、今回言及した部分は変わらない気がする。
あと top-level await しているコンポーネントは Suspense が必ず必要なのがちょっと微妙。。。

なんだかそもそも自分が Suspense に抱いている期待自体が完全にお門違いな気がしてきた。React での Suspense の実用例をいろいろ調べてみた方がよさげ。

Discussion