polidog lab

Top About Rss
2021年03月27日

Nuxt Composition APIのuseFetchとcomputedの問題について

Nuxt Composition APIでuseFetchとcomputedを組合わて使っていたらトラブルが。。。。

使っている@nuxtjs/composition-apiのバージョンについて

こんな感じ @nuxtjs/composition-api": "0.22.0"

何が起きていたのか?

https://codesandbox.io/s/vigilant-gould-45vg4?file=/pages/posts/_id.vue

import {
  defineComponent,
  useFetch,
  useContext,
  ref,
  computed,
} from "@nuxtjs/composition-api";

import Author from "~/components/Author.vue";

export default defineComponent({
  components: {
    Author,
  },
  setup() {
    const post = ref();

    const { $http, params } = useContext();

    const computedTitle = computed(() => {
      if (post.value) {
        return `Computed: ${post.value.title}`;
      }

      return "Computed: ...";
    });

    useFetch(async () => {
      post.value = await $http.$get(
        `https://jsonplaceholder.typicode.com/posts/${params.value.id}`
      );
    });

    return { post, computedTitle };
  },
});

上記のようなcomputedとuseFetchを使う実装はは以下のような警告が出てしまう。

Write operation failed: computed value is readonly

何が問題なのか?

nazoさんのtweetにもありましたが意図せずにcomputedのsetが呼ばれてしまうらしい。

問題の箇所のコードを確認してみるとuseFetchのonBeforeMountでsetされてしまっているのでダメらしい。

https://github.com/nuxt-community/composition-api/blob/c394e35/src/composables/fetch.ts#L318-L332

  onBeforeMount(() => {
    // Merge data
    for (const key in data) {
      try {
        if (key in vm && typeof vm[key as keyof typeof vm] === 'function') {
          continue
        }
        set(vm, key, data[key])
      } catch (e) {
        if (process.env.NODE_ENV === 'development')
          // eslint-disable-next-line
          console.warn(`Could not hydrate ${key}.`)
      }
    }
  })

この問題は1年近く放置されているようです…

どう解決するのか?

いくつか解決策あるのかなと思います。

reactiveを使う

一番既存のコードに修正方法としてはreactiveとかでラップしちゃうのが良いかなと思いました。

import {
  defineComponent,
  useFetch,
  useContext,
  ref,
  computed,
  reactive,
} from "@nuxtjs/composition-api";

import Author from "~/components/Author.vue";

export default defineComponent({
  components: {
    Author,
  },
  setup() {
    const post = ref();

    const { $http, params } = useContext();

    const title = computed(() => {
      if (post.value) {
        return `Computed: ${post.value.title}`;
      }

      return "Computed: ...";
    });

    useFetch(async () => {
      post.value = await $http.$get(
        `https://jsonplaceholder.typicode.com/posts/${params.value.id}`
      );
    });

    const data = reactive({
      title,
    });

    return { post, data };
  },
});

良いアプローチなのかは正直わかりませんが…。

watchを使ってtitleの値を更新する

computedを止めてしまうという手もありかと。

import {
  defineComponent,
  useFetch,
  useContext,
  ref,
  watch,
} from "@nuxtjs/composition-api";

import Author from "~/components/Author.vue";

export default defineComponent({
  components: {
    Author,
  },
  setup() {
    const post = ref();
    const title = ref("");

    const { $http, params } = useContext();

    watch(post, (updatedPost) => {
      title.value = updatedPost.title;
    });

    useFetch(async () => {
      post.value = await $http.$get(
        `https://jsonplaceholder.typicode.com/posts/${params.value.id}`
      );
    });

    return { post, title };
  },
});

useAysncを使う

useFetchがAPIリクエストに使っているのであれば、それはuseAsyncに置き換えることはできます。 ただ、SSGの場合でも毎回APIリクエストが走ってしまうので、SSGの場合はuseStatic使うほうがいいかも。

まとめ

  • uesFetch, computedの組み合わせは死ぬ
  • reactiveでラップしちゃえばなんとかなる
  • そもそもcomputed使わなくていいならwatch使ってなんとかする方法もある
  • 通信目的ならuseAsync or useStaticあたり使うのも良さそう。

最後に

ツイートを参考させていただいた@d_horiyama_webさん、@nazo大先生に感謝しています。
twitterまじで偉大。

comments powered by Disqus