Implement a simple caching for Relay Modern.
update - 2017/11/07: Consulting official docs may make you even happier: Add Caching docs by felippepuhle · Pull Request #2121 · facebook/relay · GitHub. It's not merged by now.
Unlike Relay Classic, Relay Modern doesn't come with an automagical caching system out of the box. Instead, it has an utility class to help you implement a cache system.
In this article I'll describe an example implementation of a simple caching. I don't explain much of details but you'd understand if you're already familiar with Relay Modern and I believe you're so as trying to implement a cache.
Code
const { Environment, Network, RecordSource, Store, QueryResponseCache, } = require('relay-runtime'); const yourEndPoint = 'https://example.com' const headers = { 'content-type' : 'application/json', } const cache = new QueryResponseCache({size: 100, ttl: 100000}); function fetchQuery( operation, variables, cacheConfig, uploadables, ) { const queryId = operation.name const cachedData = cache.get(queryId, variables); // Handle force option in RefetchOptions // See: https://facebook.github.io/relay/docs/pagination-container.html // https://facebook.github.io/relay/docs/refetch-container.html const forceLoad = cacheConfig && cacheConfig.force if (!forceLoad && cachedData) { return cachedData; } if (forceLoad) { // clear() means to reset all the cache, not only the entry addressed by specific queryId. // See blog comments for more details. cache.clear() } return fetch(`${yourEndPoint}/graphql`, { method: 'POST', headers, body: JSON.stringify({ query: operation.text, // GraphQL text from input variables, }), }).then(response => { const data = response.json(); // A cache key, queryId in this code, should be unique per query. cache.set(queryId, variables, data); return data }); } // Create a network layer with the fetch function export const network = Network.create(fetchQuery);
Related tweets.
Yes, depending on product we use QueryResponseCache (part of public API) w various TTL/size, cleared on any mutations.
— Joe Savona (@en_JS) 2017年9月1日
Building a simple request-response cache is simple enough and good for many use cases, we've found.
— Greg Hurrell (@wincent) 2017年5月8日
Yes! Here's an example on a cache implementation using #relaymodern QueryResponseCache. Full codehttps://t.co/iCDaKS1x8k@leeb @LearnRelay pic.twitter.com/bva9O30N17
— Yusinto Ngadiman (@yusinto) 2017年8月4日
Relay Modernに移行した
今作ってるアプリ、Lylica - 街のおすすめが分かるSNSをRelay ClassicからRelay Modernに移行した。
動機
今後機能を追加していくにあたり、今のうちにえいやとやっておきたかったからだ(とある機能の追加に腰が重かったから先にこっちに着手したというのは内緒)。
以前 Relay Modernへの移行検討をした - taiki-t's diary という記事を書いてその時にはいわゆるlocal stateがサポートされるまでModernに移行するつもりはないと言っていたけど、結局まだサポートされていない*1まま移行した。まあえいやって思っちゃったから。
パフォーマンス
体感的にまあアプリのパフォーマンス的にはそこまで変わりはない。計測はしてない。OptimisticResponse
はClassicの頃から最強。サーバから返ってくるであろうデータをあらかじめ定義することで、サーバからデータが返ってきたものとしてビューを更新してくれるので爆速。サーバー側のエラーなどでデータ更新ができなかった場合は自動でロールバックしてくれる。更新できたら見た目にはそのまま。さいつよさしかない。バックエンドherokuで動かしてレイテンシ800msとかあっても感じさせない、というか原理的に見ためには存在しない。やばい。
気分
気は楽になった。Classicのスタイルでコンポーネントやmutationを追加するときに「うっ」って思ってたのが消えた。 そして、来たるSubscriptionsに対応できるぞーという楽しみができた。よかった。最高。
かかった手間
これ:
計36時間 46 commits
— taiki-🇹 (@taiki__t) August 17, 2017
Relayを使った30個のコンポーネントをModernに置き換えるために使った時間
まだmutationsがある
自動生成も含めて変更されたファイルを含めると 103 files, +10,022, -1,317 linesという感じ
上記はほぼmutations以外のRelayを使ったコンポーネントを置き換えた時間。スパイク的に1箇所ぐらいmutationも書き換えたと思う。 で、上記以外のmutationsの書き換えにかかった時間は、9時間53分*2。1日かけて15個ほど書き換えた*3。
手順
親切なことに移行の手順書みたいなのが用意されているのでそれをベースにやった。
Conversion Playbook - Relay Docs
上記の手順通りにうまくいったのは Step 0: Install Relay v1.0
までだった
Step 1: Incrementally convert to Relay Compat
で、用意されていた移行スクリプト(まじ親切)を実行し、
シンプルなコンポーネントは自動でcompatモードに移行できた。
しかしその後、「手動でやらなきゃいけないよ」と上述のスクリプトが親切にコメントインしてくれた箇所は、compat
モードで動かないことが試行錯誤ののち判明した。
ので、該当箇所は1箇所ずつ手動でいきなり Modernにえいやしていくことにした。
結果として手順は
- Relay Modernのインストール
- スクリプトで一部コンポーネントをcompatモードに移行
- ファイル名をCamelCaseに変更
- 自動でできなかったところをcompatを経由せず一つ一ついきなりModernに移行
- 実験的に途中1箇所mutationを置き換え
- データ取得については全てModernに移行(Relayの読み込み元を
react-relay/compat
からreact-relay
に移行) - mutationsを一つ一つ手で書き換え
- 途中cacheを実装。Relay Modernではデータのcacheは自動でやらなくなったため。cacheの実装については別途記事にしようかと思う。
- 完全にRelay Modernに移行 (
react-relay/compat
,react-relay/classic
からの読み込みを全てreact-relay
に。)
ハマったとこ
コンポーネント、ファイル名
手順にもちょろっと書いたけど これ:
Relay Modern compatモードで動かしてるんだけど、fragment名についてcomplierではファイル名に依存してて、runtime(?)ではコンポーネント名に依存してる… snake caseとcamel caseにしてるからファイル名変えないと…
— taiki-🇹 (@taiki__t) August 9, 2017
compat(compatible とは言っていない)…! https://t.co/Pd6vE7eiJL
— Yosuke Kurami (@Quramy) August 10, 2017
ので、ファイル名とimportしてるところをガッと書き換えた。そこまでやって、一部機能がcomapモードに対応してないことが試行錯誤の結果判明したので結局いきなりModernにいくことに。
Rootのクエリ名
上記についてはまあ最初からファイル名もCamelCaseで書いてある人には関係のない話だったろう。Facebook内部ではそういう規約でやってるんだと思う。他、ルートとなるクエリの名前にもFacebook内部の仕様が漏れ出ててる雰囲気があって、Query
っていう名前じゃないとcompat
モードを通せなかった。RootQuery
ってしてたからそこもはまった。この制約はcompatモードのみで、Modernに完全に移行してしまえば関係ない。関係あったらアプリの後方互換性的にしんどかったなあ。この問題は今後のアップデートで解消される予定。
キャッシュ
キャッシュをするなら手で有効にする必要がある。キャッシュ機構自体はあるので、それをちょろちょろっと組み合わせればできる。でも知らなかったしドキュメントもなさげ。
追記 2017-09-05: キャッシュの実装についてサンプルを書いた: Implement a simple caching for Relay Modern. - taiki-t's diary
Connections
例えばモデルとしてポストがあって、ポストがたくさんコメントを持つというとき、コメントを取り扱うのがconnections。has manyな関係を取り扱う感じ。書き方がだいぶ変わってて、あとcompatモードじゃ動かなくて、ドキュメントも薄かった。まあやった。自動で変換してくれないところの一つ。てか自動で変換してくれないところはそれぞれやはりつらみがある、 Relay 1.2.0が都合よく移行中に出てきたので、それで助かった部分もある。Connectionsの再取得の書き方など、1.2.0より前は大変そうだった。
mutation成功、失敗時の処理
ClassicではonFailure
, onSuccess
的な二つのコールバックでサーバーからのエラー、成功時処理をそれぞれ書いてた。Modernになってからは見た目にはonError
とonCompleted
というコールバックになってて、そのままClassicの感じかなと思ってたら挙動が異なっていた。サーバーからのレスポンスは失敗でも成功でもonCompleted
で処理するようになっててonCompleted
でサーバーから返されたエラーのハンドリングができるとはwebのドキュメントにはなかった(レポジトリのやつは更新されてた)。onError
はRelay
がエラーに直面した時に呼ばれる。
さいごに
全体的な感想としては、やってよかったなと。書くようになったところと書かなくてよくなったところのバランスが、Classicと比べてよくなってると思う。特にmutations周りはよくできてるなーと言いたくなった。ただ、書くようになったところのドキュメントがまだ薄いので、ね。ドキュメントもっと充実するといい。
まだ色々あったようなきがするけど疲れたのでここら辺で。気が向いたら or リクエストがあれば追記しようと思う。ところでこの記事誰かの役に立つのだろうか?笑
リンク
Relay - A JavaScript framework for building data-driven React applications
GraphQL error handling in Relay Modern · Issue #1913 · facebook/relay · GitHub
[Modern] Paginating a connection in the root query · Issue #1705 · facebook/relay · GitHub
Lylica - 街のおすすめが分かるSNSを App Store で
*1:機能はあるかもしれないけどドキュメントがなく誰も使い方がわからない [Modern] Client schema docs and examples · Issue #1656 · facebook/relay · GitHub
*2:
計測には RescueTime 使ってる。いい。
*3:今は自分しか開発陣はいないので、やりたい時にやるスタイル。逆に4時間とかしか稼働しない時もある
メモ: ファイル名をキャメルケースにするRubyワンライナー
Relay Modern compatモードで動かしてるんだけど、fragment名についてcomplierではファイル名に依存してて、runtime(?)ではコンポーネント名に依存してる… snake caseとcamel caseにしてるからファイル名変えないと…
— taiki-🇹 (@taiki__t) 2017年8月9日
というわけでgraphql
を含むファイルの名前をCamelCaseにしたかった
git grep -l graphql | ruby -nle 'File.rename($_, $_.split("-").map{|w| w[0] = w[0].upcase; w}.join)'
参考
技術コミュニケーションでは「良いと思うので」に理由を添えて生産性を高めよう
「こちらの方が良いと思うのでこうします!」
「これはあんまりよくないと思うので」
というやり取りはよくある。自分も気を抜くとやってしまう。 これは暗黙的に「良い」の概念が共有されていると思いやってしまいがちだが、必ずしも共有されているわけではない。
「こちらの方が良いと思うので」とだけ言われた方は、「何で???」と思っているかもしれない。
ので、「良いと思う」だけで終わらせることはせずに、代わりに明確にその根拠を含める、添えるようにする。 そうすることで、より意図を明確に伝えることができる。 また、何をもって「良い」と考えているか伝えることでより的確なフィードバックを得ることができる。
例
1
- x「こちらのデザインの方が良いと思うのでこれで行きます。」
- ○「こちらのデザインの方がユーザーに意図を明確に伝えられて良いと思うのでこれで行きます。」
2
- x「このコードはこっちの方が良いと思います」
- ○「このコードはこっちの方がメンテナンス性が高くて良いと思います」
上記のように理由を添えることで、コミュニケーションのステップも縮めることができる。 相手の意図を把握できずモヤモヤした場合「何が良いのですか?」からいちいち探らなくて良い。 理由が気になった場合には 「どうしてこちらの方が意図を明確に伝えられるんですかね?」や「なぜメンテナンス性が高いんですかね?」 と、そのようにすぐコミュニケーションを進めることができる。 理由が気にならない場合は、OKの旨を返事すればそれで事足りる。
現場にて
- コードレビューで: 「これは良くないと思います」だけのコメントは生産性を下げる行為なので、必ず理由を添えるようにしよう
- コードコメントで: 「FIXME: このコードはよくない」みたいなのは何が良くないか忘れるし見た人もわからないので、理由を添えよう
- Slackで: 賞賛以外の「良いと思います」には理由を添えよう
ターミナルでgit grep して一括置換するコマンド
$ git grep -l 'search word' | xargs sed -i '' -e 's/search word/replacing word/g'
こんな感じ。
search word
, replacing word
はそれぞれgrepする語、置換後の語を指定。
git grep -l
で対象の語が含まれるファイル一覧を取得、 | xargs
でそれを sed
コマンドの引数渡す。
sed -i
でインプレイスで置換。
React Native Tab View with default index to 1
When Using React Native Tav View with defalut index to 1, I happened to face a strange behaviour; automatically swipes to index 0 sometimes when opening the scene.
So what I did was to set initialLayout
and it fixed it.
Like this:
const initialLayout = { height: 0, width: Dimensions.get('window').width, }; render () { <TabViewAnimated // other props initialLayout={initialLayout} /> }
React Native Tab Viewの初期indexを1に指定して使ったところ、画面を開いたとき時々勝手にindex 0の画面にスクロールされることがあった。
initialLayout
を設定したら解決した。