📝

React & Amplify で GraphQLの subscription 使ってみた「俺の理解」

2021/06/19に公開

前回書いた、とりあえず動かす!Cloud9 & React & Amplify & GraphQLの環境構築を基にGraphQLのsubscriptionについてメモがわりにまとめました。もともとAmplifyに手を出したキッカケが、「サーバからリアルタイムでデータ取りたいなぁ〜」「Reactで即時反映ってできないかなぁ〜」ってことでした。なので、この記事がある意味では本命記事になります!!

参考にした記事は前回とほぼ同じです。
https://qiita.com/G-awa/items/a5b2cc7017b1eceeb002
https://dev.classmethod.jp/articles/react-amplify-appsync-dynamodb-tutorial/
https://qiita.com/Junpei_Takagi/items/f2bc567761880471fd54

前提条件

とりあえず動かす!Cloud9 & React & Amplify & GraphQLの環境構築にあるschemaとApp.jsを基に説明します。GraphQLのsubscriptionについて書いた記事ですが、前回の記事で書いたschemaとApp.jsのsubscription部分で、ただひたすら自分が理解したことを説明するだけです。

schemaのこと

schema, スキーマと言ってますが、実態は以下のフォルダにある「schema.graphql」ファイルの説明です。

  • reactプロジェクト配下/amplify/backend/api/{api名}/

定義は以下の通りです。

type Todo 
    @model 
    @key(name: "SortByName", fields:["owner", "name"], queryField: "listTodosSortedByName" )
{
    id: ID! 
    name: String!
    owner: String
    updatedAt: AWSDateTime 
    createdAt: AWSDateTime 
}

ここで重要なのはディレクティブの「@model」です。@modelをつけるとDynamoDBにテーブルが生成されます。またそのテーブルのデータを操作するために、query, mutation, subscriptionがフロントエンドにはファイルとして、バックエンド(AppSync)にはschema定義として生成されます。@modelの後ろにquery, mutation, subscriptionの定義をそれぞれ記述しない場合は、自動で以下の定義ファイルとデータ操作に必要なリゾルバが生成されます。(詳細は公式を参照)
上記のschemaは@modelにquery, mutation, subscriptionのいずれも付与していないので、デフォルトの定義ファイルとリゾルバが生成されます。また5つの項目があるテーブルがDynamoDBに生成されます。

  • query
    • リゾルバ:get, list
  • mutation
    • リゾルバ:create, update, delete
  • subscription
    • リゾルバ:onCreate, onUpdate, onDelete

この時点での理解は、

  • schema : テーブルとそこに入るデータの型を定義するもので、定義した型以外を受け付けないように、定義した以外の操作を受け付けないように制御するもの
  • query, mutation, subscription : DynamoDBのデータ操作をするもの
    • query : 単一、または複数のデータ取得を行う
    • mutation : データの登録 / 更新 / 削除を行う
    • subscription : mutationをトリガーにバックエンドからフロントエンドへデータの即時反映(リアルタイム更新)を行う
  • リゾルバ : ↑3つのデータ操作で詳細な処理を行うメソッドのようなもの

といった程度です。(自分の理解をそのまま文字にしたので、内容は厳密な定義との大きな乖離があります。コメントとご指摘をください。)

App.jsのこと

下記のApp.jsに書き換えました。

import React, {useState, useEffect, useReducer } from 'react';

import API, { graphqlOperation } from '@aws-amplify/api';

import { createTodo } from './graphql/mutations';
import { onCreateTodo } from './graphql/subscriptions';

import './App.css';

const QUERY = 'QUERY';
const SUBSCRIPTION = 'SUBSCRIPTION';

const initialState = {
  todos: [],
};

const reducer = (state, action) => {
  switch (action.type) {
    case QUERY:
      return {...state, todos: action.todos};
    case SUBSCRIPTION:
      return {...state, todos:[...state.todos, action.todo]}
    default:
      return state;
  }
};

async function createNewTodo() {
  const todo = { name:  "Todo " + Math.floor(Math.random() * 10) };
  await API.graphql(graphqlOperation(createTodo, { input: todo }));
}

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const [user] = useState(null);

  useEffect(() => {

    const subscription = API.graphql(graphqlOperation(onCreateTodo)).subscribe({
      next: (eventData) => {
        const todo = eventData.value.data.onCreateTodo;
        dispatch({ type: SUBSCRIPTION, todo });
      }
    });

    return () => subscription.unsubscribe();
  }, []);

  return (
    <div className="App">
      <p>user: {user!= null && user.username}</p>
      <button onClick={createNewTodo}>Add Todo</button>
      <div>
        {state.todos.length > 0 ? 
          state.todos.map((todo) => <p key={todo.id}>{todo.name} ({todo.createdAt})</p>) :
          <p>Add some todos!</p> 
        }
      </div>
    </div>
  );
}

export default App;

重要な点はデータ登録の

await API.graphql(graphqlOperation(createTodo, { input: todo }));

部分と、データ登録をトリガーにしたリアルタイム反映の

const subscription = API.graphql(graphqlOperation(onCreateTodo)).subscribe({
      next: (eventData) => {
        const todo = eventData.value.data.onCreateTodo;
        dispatch({ type: SUBSCRIPTION, todo });
      }
    });

部分の2箇所です。順番に説明します。

データ登録

登録 / 更新 / 削除をトリガーにしてリアルタイムにデータの反映が行われるため、データ登録の処理はリアルタイム反映であるsubscriptionと関連がある内容です。
まず、amplifyのquery, mutation, subscriptionをReactで呼び出すために、以下のライブラリをインポートします。

import API, { graphqlOperation } from '@aws-amplify/api';
import { createTodo } from './graphql/mutations';

この時点での理解は以下2点です。

  • amplifyのデータ処理を行うgraphqlOperationを利用するため、amplifyのライブラリをReactにインポートする
  • src/graphql/mutations.jsにあるcreateTodo(リゾルバ)を登録処理で使うために、Reactにインポートする

そして下記の通り、ボタン押下によりデータ登録処理が呼び出され、mutationのcreateTodoによりデータ登録を行います。

<button onClick={createNewTodo}>Add Todo</button>

createNewTodoの処理でmutationのcreateTodoを呼び出しています。createTodoの引数に以下のtodoを指定しています。

const todo = { name:  "Todo " + Math.floor(Math.random() * 10) };

schema定義でテーブルのidはID型であるため、自動生成された値が登録されるので、引数の指定は不要ですが、nameは文字列で必須入力のため、引数に指定が必要となります。
次に説明するsubscriptionでこの「createTodo」が重要になります。

リアルタイム反映(即時更新)

やっと本題中の本題です。
subscriptionを利用するためのライブラリを以下の通りインポートします。

import { onCreateTodo } from './graphql/subscriptions';

これは登録で記載したのと同様に

  • src/graphql/subscriptions.jsにあるonCreateTodo(リゾルバ)を登録処理トリガーにして、即時反映で使うために、Reactにインポートする

リアルタイム反映のsubscriptionはmutaitonのcreate, update, deleteで定義したリゾルバ(今回はcreateのcreateTodo)をトリガーに処理が行われます。
改めてsubscriptionの処理を確認すると

const subscription = API.graphql(graphqlOperation(onCreateTodo)).subscribe({
      next: (eventData) => {
        const todo = eventData.value.data.onCreateTodo;
        dispatch({ type: SUBSCRIPTION, todo });
      }
    });

となっていて、

API.graphql(graphqlOperation(onCreateTodo)).subscribe

の部分で、mutaitonのcreate, update, deleteの内どれがトリガーとなっているのかという部分を、subscriptionで定義しています。今回、そのトリガーとなるイベントはmutationのcreateした際に呼び出されるonCreateです。
また、subscribeの引数「eventData」を利用して、「onCreateした時登録したデータ」をsubscriptionの処理で利用できます。今回は画面表示しているだけですが、登録されたデータによって処理を条件分岐するなど、利用の方法はたくさんありそうです。(多分、、)

終わりに

自分の頭の中にある「amplifyのsubscriptionはこういう理解だ!」という部分を書き出してみました。厳密な定義とは乖離しますが、とりあえず動くものを作る程度であれば、自分にはこの理解でも十分だったかなと思います。

Discussion