masalibの日記

システム開発、運用と猫の写真ブログです

React Apollo Mutationをやってみた

React Apolloでデータの取得(query)ができたので更新(Mutation)処理に挑戦してみました。 ちなみに更新(Mutation)ができれば新規追加もできると思われます(サンプルソースより)

事前作業

githubリポジトリにstarをつけたり、外したりするためにはgithubのtokenのscopes(スコープ)を変更しないといけなかった。 もしscopes(スコープ)を変更せずにstarをつける処理をすると以下のエラーがでます

Uncaught (in promise) Error: Your token has not been granted the required scopes to execute this query. 
The 'addStar' field requires one of the following scopes: ['public_repo', 'gist'],
 but your token has only been granted the: [''] scopes. 
Please modify your token's scopes at: https://github.com/settings/tokens

f:id:masalib:20201108172424p:plain

public_repoにチェックしてUpdateのボタンを押す

実際のソース

クリックすると展開されます(長文なので注意)

import React from 'react'
- import {gql, useQuery,useMutation} from '@apollo/client';
+ import {gql, useQuery} from '@apollo/client';
import { Buffer } from 'buffer';
import {TailSpin} from '@bit/mhnpd.react-loader-spinner.tail-spin';

// 発行する GraphQL クエリ
const GET_REPOSITORIES = gql`
  query searchRepositories($first: Int,$last: Int,$query: String!,$after:String,$before:String, ){
    search(query: $query, type: REPOSITORY, first: $first, last: $last, after:$after,before:$before) {
      repositoryCount
      pageInfo{
        endCursor
        hasNextPage
        hasPreviousPage
        startCursor
      }
      edges {
        cursor
        node {
          ... on Repository {
            id
            name
            url
            stargazers {
              totalCount
            }
            viewerHasStarred
          }
        }
      }
      }
  }
`;

+ export const ADD_STAR = gql`
+   mutation addStar ($input: AddStarInput!) {
+     addStar (input: $input) {
+       starrable {
+         id
+         viewerHasStarred
+       }
+     }
+   }
+ `

+ export const REMOVE_STAR = gql`
+   mutation removeStar ($input: RemoveStarInput!) {
+     removeStar(input: $input) {
+       starrable {
+         id
+         viewerHasStarred
+       }
+     }
+   }
+ `

const SearchRepositories = props => {
  console.log(props.query)
   // GraphQL のクエリを実行
+   const {loading, error, data, fetchMore, refetch} = useQuery(GET_REPOSITORIES, 
            {variables:
                { first: 10 ,
                  last:null,
                  query:props.query,
                  after:null,
                  before:null
                },
            },
            {fetchPolicy: "cache-and-network",}
            );

+   //starをつける系の関数
+   const [
+     addStartRepositories,
+     { loading: mutationAddStarLoading, error: mutationAddStarError }
+   ] = useMutation(ADD_STAR, {
+       onCompleted() {
+         refetch();
+       }
+     }
+   );

+   const addStart = (nodeid) => {
+     console.log("addStart start")
+     console.log(nodeid)
+     addStartRepositories({ variables: { input: { starrableId: nodeid } } , } );
+     console.log("addStart end")
+   }

+   //starをはずす系の関数
+   const [
+     removeStartRepositories,
+     { loading: mutationRemoveStarLoading, error: mutationRemoveStarError }
+   ] = useMutation(REMOVE_STAR, {
+     onCompleted() {
+         refetch();
+       }
+     }
+   );

+   const removeStar = (nodeid) => {
+     console.log("removeStar start")
+     console.log(nodeid)
+     removeStartRepositories({ variables: { input: { starrableId: nodeid } }, } );
+     console.log("removeStar end")
+   }

// クエリ実行中の表示(Loading)
  if (loading) return + <TailSpin 
  color={"black"} 
  height={150} 
  width={150} 
  />;

  // エラー発生時(レスポンスがないとき)の表示
  if (error) return <p style={{color: 'red'}}>{error.message}</p>;

  // クエリの結果が返ってきたときの表示
  const {repositoryCount} = data.search;
  const search = data.search
  const {hasNextPage,hasPreviousPage,endCursor,startCursor} = data.search.pageInfo
  console.log(hasNextPage)
  console.log(hasPreviousPage)
  console.log(Buffer.from(startCursor, 'base64').toString())
  console.log(Buffer.from(endCursor, 'base64').toString())

return (
    <>
        <h2>SearchRepositories: {repositoryCount}</h2>
+         {mutationAddStarLoading && <p>Startを追加中です...</p>}
+         {mutationAddStarError && <p>Startを追加に失敗しました : もう1度やり直してください</p>}  
+         {mutationRemoveStarLoading && <p>Startを外しています...</p>}
+         {mutationRemoveStarError && <p>Startを外すのに失敗しました : もう1度やり直してください</p>}  

        {
            //repository data view 
            search.edges.map(edge => {
                const node = edge.node
                return (
                    <li key={node.id}>
                    <a href={node.url} target="_blank" rel="noopener noreferrer">{node.name}</a>

+                     ☆{node.stargazers.totalCount}
+                     {!node.viewerHasStarred && 
+                     (
+                     <button onClick={() => addStart(node.id)}>
+                       スターをつける
+                     </button>
+                     )}

+                     {node.viewerHasStarred && 
+                     (
+                     <button onClick={() => removeStar(node.id)}> 
+                       スターを外す
+                     </button>
+                     )}
                    </li>
                )
                })
        }
        {hasNextPage && 
          (
            <button
                    onClick={async() =>
                      {
                        console.log("data get start");
                        await fetchMore({
                          variables: {
                            first: null ,
                            last:10,
                            query:props.query,
                            after: endCursor,
                            before:null
                          },
                        }, 
                        );
                        console.log("data get end");
                      }}
            >
              More 
            </button>
          )
        }
        {hasNextPage && 
          (
            <button
              onClick={() =>
                fetchMore({
                    variables: {
                        first: 10 ,
                        last:null,
                        query:props.query,
                        after: endCursor,
                    before:null
                  },
                updateQuery: (prevResult, { fetchMoreResult }) => {
                    //単純な次のページのデータの場合      
                    return fetchMoreResult;
                    },
                })
              }
            >
              NextPage
            </button>
          )
        }

        {hasPreviousPage && 
          (
            <button
              onClick={() =>
                fetchMore({
                  variables: {
                      first: null ,
                      last:10,
                      query:props.query,
                      after: null,
                      before:startCursor
                    },
                updateQuery: (prevResult, { fetchMoreResult }) => {
                  //単純な次のページのデータの場合      
                    return fetchMoreResult;
                  },
                })
              }
            >
              PreviousPage
            </button>
          )
        }
    </>
  )
};

export default SearchRepositories

解説1

{node.stargazers.totalCount}
{!node.viewerHasStarred && 
(
    <button onClick={() => addStart(node.id)}>
    スターをつける
    </button>
)}

{node.viewerHasStarred && 
(
    <button onClick={() => removeStar(node.id)}> 
    スターを外す
    </button>
)}

viewerHasStarredは表示しているユーザーが対象のリポジトリにスターを付けているのかを指します。 スターがついていない場合はaddStartで、ついている場合はremoveStarの関数を呼ぶボタンを表示しています。

解説2

export const ADD_STAR = gql`
  mutation addStar ($input: AddStarInput!) {
    addStar (input: $input) {
      starrable {
        id
        viewerHasStarred
      }
    }
  }
`

スターをつけるのはシンプルでIDをしてするだけです

書式としては

addStar(input: AddStarInput!): AddStarPayload
Adds a star to a Starrable.

戻り値

clientMutationId: String
A unique identifier for the client performing the mutation.

starrable: Starrable
The starrable.

 


 
実際のmutation処理は以下です。

クエリー

mutation addStar($input: AddStarInput!) {
  addStar(input: $input) {
    starrable {
      id
      viewerHasStarred
    }
  }
}

変数(variables)

{
  "input": { "starrableId": "MDEwOlJlcG9zaXRvcnkxMzc3MjQ0ODA=" } 
} 

結果

{
  "data": {
    "addStar": {
      "starrable": {
        "id": "MDEwOlJlcG9zaXRvcnkxMzc3MjQ0ODA=",
        "viewerHasStarred": true
      }
    }
  }
}

Starをはずす処理はaddの部分をremoveに変えるだけです。
ちなみにStarがついている状態でaddStarをしてもエラーにはなりません。

解説3

//starをつける系の関数
const [
        addStartRepositories,
        { loading: mutationAddStarLoading, error: mutationAddStarError }
    ] = useMutation(ADD_STAR, {
            onCompleted() {
                refetch();
            }
        }
    );

const addStart = (nodeid) => {
    console.log("addStart start")
    console.log(nodeid)
    addStartRepositories({ variables: { input: { starrableId: nodeid } } , } );
    console.log("addStart end")
}

addStartRepositoriesはgithubAPIを叩く関数です。
この関数はuseMutationというApolloが提供している関数をもちいて作成してます。 useMutationが実行され成功されるとコールバック関数でonCompletedを呼びだし refetch()を呼ぶ事で更新した内容を反映したリストを再取得しています。

refetch()はリスト取得のクエリーで作る関数になります

const {loading, error, data, fetchMore, refetch} = useQuery(GET_REPOSITORIES, 

 


 
addStartはユーザーがスターをつけるボタンを押した時に起動する関数です。 ボタン側に書いてもよかったのですが、個人的に見にくいのでこの形にしました  


  以下の変数はクエリーと同じでローディング中とエラーの変数です。

{ loading: mutationAddStarLoading, error: mutationAddStarError }

loading: mutationAddStarLoadingは更新中にTrueになります。
error: mutationAddStarErrorはエラーになった場合にTrueになります。

実際の使用例は以下のとおりです。

{mutationAddStarLoading && <p>Startを追加中です...</p>}
{mutationAddStarError && <p>Startを追加に失敗しました : もう1度やり直してください</p>}  

Starをはずす処理はaddの部分をremoveに変えるだけです。

結果

f:id:masalib:20201108192730g:plain
スターをつけたり外したりしています

現時点でいけていないのは2ページ目にいる状態の場合にスターをつけたり、外したりする処理をすると 1ページ目にもどってしまう・・・

refetchに変数を設定しないといけないもよう・・・
あう・・・

refetch

(variables?: TVariables) => Promise<ApolloQueryResult>

クエリを再フェッチし、オプションで新しい変数を渡すことができる関数

参考URL

https://www.apollographql.com/docs/react/data/mutations/#executing-a-mutation https://codesandbox.io/s/mutations-example-app-final-tjoje?file=/src/index.js:871-879 https://qiita.com/koedamon/items/dae8c865b19281b1aa56