Reactがもっと広まって欲しいと思っている今日このごろ。React EuropeでJoseph Savona氏の講演でRelay
についての「モヤっと」がいっきにかなり解消された気がするので、要点を本編を翻訳しながら自分なりにまとめておきます。 私の理解が誤っている可能性は十二分にありえるので、ご指摘いただければ幸いです。
はじめに
ReactとFluxって組み合わせと共によく目にするのが↓の図。
矢印は一方向にしか進まないのが特徴で、わかりやすいってのがいろんなところで書かれているんですけど、 **結局データをサーバからとってくるところってどうなってるの?**ってのが疑問として残ります。つまり、図で表現すると↓の部分の仕組みがどうなっているかってところです。
その部分を、Instagramのようなサービスを例に説明しています。
クライアントはどのようにしてサーバからデータを取得すべきか
まず、Instagramのようなサービスでフィードを例にとった場合、かなりシンプルに考えた場合でこれは3つのコンポーネント(Feed
, Media
, Comment
)で構成できることがわかります。
たった3つのコンポーネントでInstagramっぽいサービスが作れる っていうのはやたらと簡単に聞こえるんですけど、この構成でデータってどうやって取ってくるの?ってとこが課題です。 よくある話を例にいろんな案を見てみます。
A案:再利用可能なエンドポイント
なるべく再利用可能なエンドポイントを用意するというやり方で、
-
/feed
各ユーザのFeedをとってくるエンドポイント -
/photo/:id
画像を取得する為のエンドポイント -
/comment/:id
コメントを取得する為のエンドポイント
のようにエンドポイントを用意するというもの。開発者からすると作りやすい手法なんですけど、サーバへのリクエストも多ければ、エントリが読み込まれる順番が↓のように順不同になるようなUXを招いてしまうこともしばしば。
B案:オールインワンなカスタムエンドポイント
それだったらオールインワンで情報提供してくれるエンドポイント用意したらいいじゃんってことで
/feed_with_photos_and_comments
全データ取得するまでLoading...
が表示されて、取得後にいっきに全部表示されるっていう、今どき考えられないようなUXですね。レスポンシブではない!
C案:色んなカスタムエンドポイント
最終的によく取られる手法が複数のいろんなカスタムエンドポイント。
-
/feed
というエンドポイントを用意 -
/feed
はMedia Component
に必要な情報を提供 -
/feed
はクエリパラメータで/feed?count=1&offset=1
のようにどれだけのMedia
情報が必要か指定できる - ↑の機能があるおかげで、全ての
Media
をいっきに取得するのではなく、別々にAPIを叩いてMedia
を取得可能
この機能を実現することで↓のような いい感じな(順にMedia
が読み込まれるような) UXが実現できます。
UXもいい感じだしこれで解決かと思いきや、この手法で実装した場合のトレードオフももちろん存在します。(こういうサービス作ったことある人なら全部 あるある な課題)
- クライアントの状態管理
-
Media
を順番通りに読み込みたかったらどうする? - スクロール中に新しい
Media
が生成されたらoffset
はどうなる?ページネーションもどうなる? - エラー時処理どうする?(タイムアウトとか)
デザイン時の課題
デザイン時のあるある課題。Comment
コンポーネントを例に考えてみます。コメントの投稿者とコメントの内容を表示している簡単なコンポーネントです。
class Comment extends React.Component {
render() {
var {comment} = this.props;
return (
<View>
<Text>{comment.author.name}</Text>
<Text>{comment.text}</Text>
</View>
);
}
}
ここに投稿者の画像も表示しよう!ってなった場合にどうなるかって言うと
class Comment extends React.Component {
render() {
var {comment} = this.props;
return (
<View>
<Image uri={comment.author.photo} />
<Text>{comment.author.name}</Text>
<Text>{comment.text}</Text>
</View>
);
}
}
1行増やすだけ!完了!
ってのはこのComment
コンポーネントの観点だけでいくと確かにそうなんですけど、その画像をどうやってサーバから取得するのかっていうところとか考え始めると、結局影響範囲ってのは計り知れなかったりします。(必要な以上にいろんなファイルをいじらないといけなくなる)
データ書き込み時の課題
データ読み込み以上に複雑なのがデータ書き込み。どんなことを考えないといけないかというと、
- あるユーザがコメントを2つ生成した場合で、2つ目のコメントがサーバに先に到着した場合はどうする?
- あるいはそのレスポンスが順不同で返ってきたらどうする?
- コメントを2つ生成して、一つ目のコメントの書き込みが失敗したらどうする?
- サーバへのリクエストが単純に遅い場合はどうする?ペンディングしているのか失敗しているのかっていうのをユーザにどう伝える?
- 複数のユーザが同時に
Media
に対してコメントした場合、そのコメントを正しい順番で表示するためにはどうする?
などなど、いろいろな課題が存在します。
D案:上以外の全く新しい方法
上のそれぞれの案を見てもわかるように、本筋とは違うところで開発者はいろんなことを考えないといけないです(いや、本当にそう思います)。それに加え、単純な変更をしようにも新しく入ったメンバーは影響範囲がどうなるかわからないのでラーニングコストが極めて高くなりがちです。コンポーネントが使う情報をサーバから宣言的(declaritive)に取得できる方法はないのか???
そこでfacebookが作ったのがRelay
です。
Relay
Relay
はGraphQL
を使っています。まず、Comment
コンポーネントで使うデータをGraphQL
で書くと↓のようになります。
// GraphQL
Comment {
text,
author {
name,
photo
}
}
// ↑によってサーバから取得できるJSONの例↓
{
text: "Excited for React Europe!",
author: {
name: "Joe",
photo: "https://..."
}
}
これをRelay
とComment
コンポーネントを組み合わせた場合にどのような実装になるかというと、↓みたいになります。
class Comment extends React.Component {...} // 中身は省略
module.exports = Relay.createContainer(Comment, {
queries: {
comment: graphql`
Comment {
text,
author {
name,
photo
}
}
`
}
});
今までReact
で作ったコンポーネントは そのまま使えます 。Relay
はcreateContainer
の第一引数にReact
のコンポーネントを受け付けて、そのコンポーネントに必要なprops
をサーバから取得できた情報を基に渡してくれる役目を果たしてくれるのです。
概要(単一コンポーネント)
通常、React
のコンポーネントは自身が使う情報をprops
という形で受け付けます。このprops
は コンポーネントを使う側が、適切な情報を渡す責任があります。
使う側が情報を取得してこないといけない(ajax
やらでとってきてからprops
として渡してあげにといけない)ので、これが意外とめんどくさかったりするんですけど、Relay
は、React
のコンポーネントをラップして、サーバから必要な情報を取得した後にコンポーネントにprops
として渡してくれる役割を果たしてくれます。
つまりRelay
は例えばコメントのid
をprops
として受けとって、そのid
をもとにサーバにリクエストを投げて、その結果をComment
コンポーネントのprops
として渡してくれるんです。
関数型な考え方で表現すると、通常のReact
コンポーネントは
UI = view(props)
という式で表現できるのに対して、Relay
を使った場合は
UI = view(query(props))
という式で表現することができます。
概要(複数のコンポーネント)
React
を使っていくと、コンポーネント基本的に様々なコンポーネントで構成されています(コンポジション)。例えばMedia
コンポーネントにはComment
コンポーネントが含まれています。Media
コンポーネントのコードは、↓のように書けます。
class Media extends React.Component {
render() {
var {media} = this.props;
return (
<View>
<Header user={media.author} />
<Image uri={media.uri} />
<View>
{media.comments.map(comment =>
<Comment comment={comment} />
)}
</View>
</View>
);
}
}
これにRelay
を適用すると↓のようになります。
class Media extends React.Component {...} // 省略
module.exports = Relay.createContainer(Media, {
queries: {
media: graphql`
Media {
comments(first: 10) {
edges {
node {
${Comment.getQuery('comment')}
}
}
}
}
`
}
});
Relay
で包むことで以下のことが起きます
- エンドポイントから
Media
を取得 - その際、その
Media
にくっついてる最初の10個のcomment
を取得 - 結果を
Media
コンポーネントのprops
として渡す
GraphQL
の構文についてはこのエントリで説明しません。全く知らない人は、とりあえず「こんなJSONがほしいの!」ってクエリを投げたらその形のJSONがサーバから返ってくるっていうふわっとした理解でこの場はOKです(多分)。
何が素晴らしいかって、特に素晴らしいのはやっぱりRelay
が勝手にReact
コンポーネントにいい感じにprops
を渡してくれるところでしょう。例えばMedia
コンポーネントが新たにもっと情報がほしくなったときはRelay
のGraphQL
を書き換えることで魔法のようにprops
が新たに提供されることになるんです。今までのように、自前で作ったajax
の処理を触ったことによって、 思わぬところに影響を及ぼしてしまうという問題から解放されます。
さらに、Relay
はコンポーネントのrender
が走る前に要求されたデータを取得済みにします。図で表現した場合↓のような流れになっています。
これって、でもようするに B案:オールインワンなカスタムエンドポイント と一緒なんじゃないの?って疑問が浮かび上がってきます。render
前に全情報を取得するってことは、その情報が全部揃わなかったら画面が表示されないってことになりますよね。
この問題を解決する方法もRelay
には準備されています。
まず、先ほどのMedia
コンポーネントを↓のようにリファクタします。コメントを管理するためのコンポーネントCommentList
を用意しました。
class Media extends React.Component {
render() {
var {media} = this.props;
return (
<View>
<Header user={media.author} />
<Image uri={media.uri} />
<CommentList comments={media.comments} />
</View>
);
}
}
あとは、このコンポーネントでcomments
があれば表示するし、そうでなければスピナー(読み込み中)を表示するロジックを入れておきます。
class Media extends React.Component {
render() {
var {media} = this.props;
var comments;
if (media.comments) {
comments = <CommentList comments={media.comments} />;
} else {
comments = <Spinner />
}
return (
<View>
<Header user={media.author} />
<Image uri={media.uri} />
<CommentList comments={media.comments} />
</View>
);
}
}
では、Relay
は全部の情報が取得できてからコンポーネントをrender
するのですから、このままではSpinnerが表示されることはありません。そこで、どの情報は 遅延してもいいかというのをGraphQL
でdefer
を使って指定できるのです。↓のようになります。
class Media extends React.Component {...} // 省略
module.exports = Relay.createContainer(Media, {
queries: {
media: graphql`
Media {
${CommentList.getQuery('comments').defer()},
... // 他のロジックは省略
}
`
}
});
こうすることで↓のようなUXが実現可能になります。(コメントがあとから読み込まれている)
データの書き込みについて
サーバサイドで何が起きているか、までは詳しく述べられていませんが、概念てきには以下のような流れになるようです。(サーバサイドで何が起きているかはGraphQL
をもっと勉強する必要がありそう。。。)
- なんらかの
Action
が発生(例:登録
ボタンのクリックとか) -
Relay
が検知して、クエリをサーバに発行 - サーバからレスポンスを取得
- 対象コンポーネントに
props
として情報を提供
再描画の検知方法について
React
は様々なコンポーネントが組み合わさって作られていますが、Relay
を使った場合にあるデータに変化があった場合にどのコンポーネントを再描画すべきなのか、というのはどのように判断されているのでしょうか???
Flux
だったらコンポーネントがStore
の特定のイベントを監視して、変化があった場合にprops
が変わるので再描画が走ります。よって、たいていの場合コンポーネントマウント時にイベント購読して、コンポーネントがアンマウントされるタイミングでイベントの購読を解除します(開発者にそれを定義する義務があります)。
Relay
はそれらを自動でやってくれるのに加えて、 どのコンポーネントがどのデータを必要としているかという情報を持っているので、データに変化があった場合にどのコンポーネントを再描画すべきかを 自動で判定してくれます。コンポーネントのshouldComponentUpdate
もいい感じに管理してくれるので、再描画が不要なコンポーネントもすべて管理してくれます。
結局Relay
ってなんなの?
Relay
の立ち位置について考えるよりも、Relay
を使うことでどういった課題が解決できるか、というのをまとめています。
今まで開発者が抱えていた問題
- どうやってデータを取得すべきか
- サーバへのリクエスト時の整合性をどうすべきか
- サーバへの書き込み時の整合性をどうすべきか
- データの再取得をどうすべきか
- エラー時処理をどう考えるべきか
- サーバサイド・クライアントサイドの同期をどうとるべきか
- 画面の再描画をどう扱うべきか
- データのキャッシュをどう管理すべきか
- ↑の内容を頭の中でどう整理すべきか。。。
Relay
を導入するとどうなるか
- どうやってデータを取得すべきか
-
サーバへのリクエストの整合性をどうすべきか→どの順番でデータを取得すべきか サーバへの書き込みの整合性をどうすべきかデータの再取得をどうすべきかエラー時処理をどう考えるべきかサーバサイド・クライアントサイドの同期をどうとるべきか画面の再描画をどう扱うべきかデータのキャッシュをどう管理すべきか↑の内容を頭の中でどう整理すべきか。。。
かなり大胆なことを言ってますが、
Good APIs simplify concerns.
Declarative APIs eliminate them.
優れたAPIはいろんな懸念事項をシンプルにしてくれる。
宣言的なAPIにはそもそも懸念事項が存在しない。
(↑意訳しすぎ???)
React
がDOM
の懸念事項を抹殺してくれたのと同じようにRelay
はデータ取得時の懸念事項を抹殺してくれるそうです。夢のような話ですね。facebookではproductionで使われている(GroupsとかAds Managerアプリは実際にRelay
で作られているようです)ので、 **これは夢物語じゃなくてまじだ!**って言ってますけど、実際に使ったことがまだ無いので私はまだ半信半疑です(データ再取得とかエラー時処理とか実際はどうなってるのかとか)。。。が、上の内容が本当だとしたらとんでもないことになりそうですね!
ちなみにRelay
のTechnical Previewは先日オープンソース化されているので、気になる方は色々と触ってみるしかないですね!私もワクワクが止まりません!(GraphQL
の勉強も必須ですね)
ここまで読んでくださった方、ありがとうございます。冒頭にも書いていますが、理解が誤っている点や、わかりづらい点、誤字脱字等指摘がありましたらコメント欄にてお願いします。