こんにちは、mabuiです。
前回に続いて今回はApollo ClientのQueryコンポーネントについて、GraphQLからデータを取得してUIに反映させる方法、エラーとローディング状態中の処理方法を見ていきます。
Apollo Clientを使用することで、ローディング中の処理も簡潔に書くことができます。
公式Queryチュートリアルページを部分的に意訳しながら、翻訳した内容になっています。
クエリコンポーネント
Query
コンポーネントを作成するには、query
属性(=this.props.query
)にgql
関数でラップしたGraphQLクエリ文字列を渡し、children
属性(=this.props.children
)にレンダリングする要素を与えます。
children
属性に渡された要素は、Query
コンポーネント内部でrender prop
関数に渡されてレンダリングされます。
children
属性はQuery
コンポーネントの要素として定義する必要はなく、コンポーネントの内部に直接置くことができます。
1 2 3 4 5 6 7 |
<Query> {/* children */} {({ data }) => ( <p>query result: {data}</p> )} </Query> |
children
属性の定義内では、UIのレンダリングに使用できるロード、エラー、およびデータの属性を含むApollo Clientのオブジェクトが提供されます。
下記にselect要素で犬種を選択できるコンポーネントのサンプルコードを記述します。
Dogs.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import React from "react"; import gql from "graphql-tag"; import { Query } from "react-apollo"; const GET_DOGS = gql` { dogs { id breed } } `; const Dogs = ({ onDogSelected }) => ( <Query query={GET_DOGS}> {({ loading, error, data }) => { if (loading) return "Loading..."; if (error) return `Error! ${error.message}`; return ( <select name="dog" onChange={onDogSelected}> {data.dogs.map(dog => ( <option key={dog.id} value={dog.breed}> {dog.breed} </option> ))} </select> ); }} </Query> ); export default Dogs; |
App.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import React, { Component } from 'react'; import './App.css'; import ApolloClient from "apollo-boost"; import { ApolloProvider } from "react-apollo"; import Dogs from './components/Dogs'; const client = new ApolloClient({ uri: "https://nx9zvp49q7.lp.gql.zone/graphql" }); class App extends Component { render() { return ( <ApolloProvider client={client}> <div> <h2>My first Apollo app 🚀</h2> <Dogs/> </div> </ApolloProvider> ); } } export default App; |
Dogs
コンポーネントをApp
コンポーネント内でレンダリングして、selectフォームを表示しています。
フォームの値が変更されると、値がthis.props.onDogSelected
経由で親コンポーネントに送信され、以降に出てくるDogPhoto
コンポーネントに渡されます。
では、次の章でDogPhoto
コンポーネントを構築し、フォームの変数を使ったより複雑なクエリを実行します。
データの受け取り
Query
コンポーネントがマウントされると、まずApolloキャッシュからのクエリ結果のロードを試し、そこにない場合はサーバーにリクエストを送信します。
データが戻ってくると、それを正規化してApolloキャッシュに保存します。
Query
コンポーネントは結果をサブスクライブしているため、データをリアクティブに更新します。
Apollo Clientのキャッシュの動作確認のために、DogPhoto
コンポーネントを作成します。
DogPhoto
コンポーネントはDogs
コンポーネントからbreed
属性を引き継ぎます。
DogPhoto.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import React from "react"; import gql from "graphql-tag"; import { Query } from "react-apollo"; const GET_DOG_PHOTO = gql` query Dog($breed: String!) { dog(breed: $breed) { id displayImage } } `; const DogPhoto = ({ breed }) => ( <Query query={GET_DOG_PHOTO} variables={{ breed }}> {({ loading, error, data }) => { if (loading) return null; if (error) return `Error!: ${error}`; return ( <img src={data.dog.displayImage} style={{ height: 100, width: 100 }} /> ); }} </Query> ); export default DogPhoto; |
App.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import React, { Component } from 'react'; import './App.css'; import ApolloClient from "apollo-boost"; import { ApolloProvider } from "react-apollo"; import Dogs from './components/Dogs'; import DogPhoto from './components/DogPhoto'; const client = new ApolloClient({ uri: "https://nx9zvp49q7.lp.gql.zone/graphql" }); class App extends Component { state = { selectedDog: null }; onDogSelected = ({ target }) => { this.setState(() => ({ selectedDog: target.value })); }; render() { return ( <ApolloProvider client={client}> <div> <h2>My first Apollo app 🚀</h2> {this.state.selectedDog && ( <DogPhoto breed={this.state.selectedDog} /> )} <Dogs onDogSelected={this.onDogSelected}/> </div> </ApolloProvider> ); } } export default App; |
onDogSelected
関数がDogs
コンポーネントに渡され、selectフォームの変更時にselectedDog
ステートが変更されます。
DogPhoto
コンポーネントのbreed
属性にはselectedDog
ステートが渡されて、内部のQuery
コンポーネントのvariables
属性にそれが渡り、クエリの変数として使用され、ステート変更のたびに犬の画像が変更されます。
犬の画像を選択して、他の犬を選択後に再度最初の犬を選択してみましょう。
(ex. akita → beagle → akita)
同じ犬の選択時に画像の読み込みが発生しないことが分かります。
これは、最初の選択時に画像はApolloのキャッシュに乗るためです!
ポーリングと再フェッチ
キャッシュを使用するのではなく、新しいデータが必要な時の手段として、ポーリングと再フェッチがあります。
ポーリングは、クエリを指定された間隔で再取得させることにより、ほぼリアルタイムのデータを取得するのに役立ちます。
ポーリングを実装するには、pollInterval
属性をQuery
コンポーネントに渡し、間隔をmsで指定します。
0を渡すと、クエリはポーリングしません。
render prop
関数に渡される結果オブジェクトに対してstartPolling
関数とstopPolling
関数を使用して、動的ポーリングを実装することもできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const DogPhoto = ({ breed }) => ( <Query query={GET_DOG_PHOTO} variables={{ breed }} skip={!breed} pollInterval={500} > {({ loading, error, data, startPolling, stopPolling }) => { if (loading) return null; if (error) return `Error!: ${error}`; return ( <img src={data.dog.displayImage} style={{ height: 100, width: 100 }} /> ); }} </Query> ); |
pollInterval
を500に設定すると、0.5秒ごとに新しい犬の画像が表示されます。 ポーリングは、GraphQLサブスクリプションの設定を複雑にすることなく、ほぼリアルタイムのデータを得る優れた方法です。
ポーリングの代わりにユーザーアクションに応答してクエリを再読み込みする場合はrefetch
関数を使用します。 ここでは、DogPhoto
コンポーネントにボタンを追加し、クリックするとrefetch
関数をトリガーします。 refetch
関数はvariables
属性を引数に取りますが、新しいvariables
属性を渡さないと、以前のクエリのものと同じvariables
属性が使用されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
const DogPhoto = ({ breed }) => ( <Query query={GET_DOG_PHOTO} variables={{ breed }} skip={!breed} > {({ loading, error, data, refetch }) => { if (loading) return null; if (error) return `Error!: ${error}`; return ( <div> <img src={data.dog.displayImage} style={{ height: 100, width: 100 }} /> <button onClick={() => refetch()}>Refetch!</button> </div> ); }} </Query> ); |
ローディングとエラーステート
再フェッチまたはポーリングを実行しているときのロード状態を取得するには、render prop
関数内の結果オブジェクトのnetworkStatus
属性を使用して、クエリのステータスに関する細かい情報を取得します。
また、notifyOnNetworkStatusChange
属性をtrueに設定して、クエリ再実行中にQuery
コンポーネントが再レンダリングするようにする必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
const DogPhoto = ({ breed }) => ( <Query query={GET_DOG_PHOTO} variables={{ breed }} skip={!breed} notifyOnNetworkStatusChange > {({ loading, error, data, refetch, networkStatus }) => { if (networkStatus === 4) return "Refetching!"; if (loading) return null; if (error) return `Error!: ${error}`; return ( <div> <img src={data.dog.displayImage} style={{ height: 100, width: 100 }} /> <button onClick={() => refetch()}>Refetch!</button> </div> ); }} </Query> ); |
networkStatus
プロパティは、異なるロード状態を表す1〜8の数値を持つ列挙型です。 4は再フェッチに対応しますが、ポーリングとページ番号の数もあります。 使用可能なすべてのロード状態の完全なリストについては、リファレンスガイドを参照してください。
ロード状態ほど複雑ではありませんが、コンポーネント内のエラーへの応答は、Query
コンポーネントのerrorPolicy
属性を使用してカスタマイズすることもできます。 errorPolicy
のデフォルト値は none
で、すべてのGraphQLエラーをランタイムエラーとして扱います。 エラーが発生した場合、Apollo Clientは要求に応じたデータを破棄し、render prop
関数のerror
属性をtrue
に設定します。 エラー情報と共に部分データを表示する場合は、errorPolicy
を all
に設定します。
手動でクエリを実行する
ReactがQuery
コンポーネントをマウントすると、Apollo Clientは自動的にクエリを実行します。
ユーザーがボタンをクリックするなどのアクションを実行するまでクエリの実行を延期したい場合は、ApolloConsumer
コンポーネントを使用し、client.query()
を直接呼び出す必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import React, { Component } from 'react'; import { ApolloConsumer } from 'react-apollo'; class DelayedQuery extends Component { state = { dog: null }; onDogFetched = dog => this.setState(() => ({ dog })); render() { return ( <ApolloConsumer> {client => ( <div> {this.state.dog && <img src={this.state.dog.displayImage} />} <button onClick={async () => { const { data } = await client.query({ query: GET_DOG_PHOTO, variables: { breed: "bulldog" } }); this.onDogFetched(data.dog); }} > Click me! </button> </div> )} </ApolloConsumer> ); } } |
この方法だと冗長になるので、可能な限りQuery
コンポーネントを使用することをお勧めします。