🐈

俺でもわかるGraphQLで基本のTodoアプリをSvelteとApolloでやってみる(5)

2021/06/09に公開

はじめに

以前につくったREST-APIベースのTodoアプリをGraphQLで作ってみるとどんな感じかやってみるという回です。

Apollo、Svelte、svelte-apolloで進めています。この構成では日本語の情報少なめなのでこういうのもあってもいいのではと思っています。

前回で、svelte-apollo 完全に理解した 気分になっていたところ、実際に簡単なCRUDアプリ作ってみると意外と考えることがあったので、その辺りを書いておこうかと思ってます。

できあがり

以前のやつとほとんど同じUIですが、中身はGraphQLに入れ替わっています。

gtodo

コード

Apollo, Svelteともにコードはこちら。いけてないところ教えてくれると嬉しいっす。
https://github.com/narutaro/svelte-apollo-todo

考えたこと

簡単なアプリなんですぐできると思ってたんですが、ちょいちょいハマったところを残しておきます。ほとんどは今回使ったsvelte-apolloの話[1]

QueryをEventHandlerでコールしない

タスクを取りに行くところ。

const getTask = async (tid) => {
  const reply = await query(get_task, { variables: { id: tid } });
  reply.subscribe(data => console.log('get', data));
};
getTask(1);

このまま実行したら動きます。

<i class="fas fa-info" on:click={ () => getTask(task.id) }></i>`

でも、こんな風にAjax的にクリックからgetTask()を実行したら、怒られます。

Uncaught (in promise) Error: Function called outside component initialization

どうやら、svelte-apollosetClient()はコンポーネントの初期化の時にしか有効ではなく、EventHandlerからは扱えないみたいです[2]

となると、クエリの引数や値が変わった時の再取得とかはどうすんの?とおもったらsvelte-apolloのREADMEにもデータのBindingsでrefresh()使う方法が書いてあった。なるほど以下のような方法を想定しているのか。

let tid = 1;
const getTask = (taskId) => { tid = taskId; }

const first_task = query(get_task);
$: selected_task = first_task.refetch({ id: tid })

としておいて

<i class="fas fa-info" on:click={ () => getTask(task.id) }></i>

で、tidを変更して、reactiveに$:を動かしてクエリで値を再取得する。

Storesの入ったPromiseが返ってくる

query()の返り値はstoreが入ったPromiseです。一方、そのQueryの再実行であるrefetch()の返り値は普通のPromiseで、mutaion()の返り値も普通のPromiseです[3]

え?...ということはquery()refetch()では返り値をHTMLにマップするときにやり方が変わるってことか...

例えば、以下のテンプレはstoreが入ったPromiseであるbookの中身を$、つまりsubscribeで参照してるので、refetch()した時には使えないということです。今回のような同じテーブルに対して値を状態に合わせて入れ替えるケースにはquery()refetch()に揃えておかないとちょっと書きにくいです。今回は全部refetch()で揃えることにしてみました。

<ul>
  {#if $books.loading}
    <li>Loading...</li>
  {:else if $books.error}
    <li>ERROR: {$books.error.message}</li>
  {:else}
    {#each $books.data.books as book (book.id)}
      <li>{book.title} by {book.author.name}</li>
    {/each}
  {/if}
</ul>

キャッシュがようわからん

Apolloは賢いのでクエリの結果をクライアント側でキャッシュしてくれます。例えば、こんなクエリでactiveなTodoだけ出すかどうかをリアクティブにスイッチできます。でもこれだけだとキャッシュをみてるので、Mutation、つまりTodoの作成や削除、変更があったときに反映できません。なんとかしないと。

$: current_task = query(list_tasks, {
  variables: {
    activeOnly: activeOnly
  }
});

Apolloのキャッシュ戦略については、ここに素晴らしいまとめがありますので書くことはあまりないです。このうちのどれかを適用すればいいのです。

https://yigarashi.hatenablog.com/entry/apollo-client-cache-mutation

キャッシュを使わないようにする

まずは一番簡単そうなキャッシュを一切使わないようにして逃げようと思います。fetchPolicyのパラメータをいじれば良さそうです。

$: current_task = query(list_tasks, {
  variables: {
    activeOnly: activeOnly,
    fetchPolicy: "network-only"
  }
});

...やってみたんですが、fetchPolicy: "network-only" fetchPolicy: "no-cache"ともにquery()の結果は引き続きキャッシュを見ているようです。うーん、よくわからない。残念。

Mutationをトリガーにサーバから再取得する

キャッシュの設定で逃げることができなかったので、次はMutationが発生するたびにrefetch()することにします。これが一番確実な気がする。問題は、どうやってMutationが発生したことをSvelteに伝えるか...ここはひとまず安易にカウンターを作ります。

let mcnt = 0; // mutation counter to trigger reactivity

const initial_tasks = query(list_tasks);

$: {
     current_tasks = initial_tasks.refetch({ activeOnly: activeOnly });
     console.log(mcnt); // refer mcnt in order to trigger this whole $: 
}

これで、on:click()などでmcntをカウントアップしておけば$:が発火するはずなので、あとは引数を適切に設定すればいいでしょう。こんなんでいいんだろうか?

次回

よくわからないところもありますが、とりあえず動くようにはなりました。ああ、そういう風に使うことを想定しているのかって感じで、初心者には学びも多かった。一方で、普通にapolloクライアント使った方がいいかも...とも思い始めたな...

いずれにしても、GraphQLは面白い技術。次はもう少し実用的なアプリをつくることにします。

シリーズ

脚注
  1. もしくは私のJSの知識不足... ↩︎

  2. 詳細はよくわかってません。どなたか強い人、この二つのやり方で何が変わるのか教えてください... ↩︎

  3. 語彙力... ↩︎

Discussion