React Apollo Hooks を使った fetch したデータを元に Update するパターンの構造
Tweet実装コード: GitHub Repository
前回の React.TypeScript 勉強会での発表 では、React apollo hooks と useReducer, useContext を用いたデータ fetch と状態管理の手法を紹介しました。発表の要点は以下の二つになります。
- Apollo Client と Codegen を用いることで「型安全な」データの取得が可能になる。
- useContext, useReducer を用いることで状態管理の適切なスコープを実現できる。
上記の発表内容に準じた実装をすることで、データをフェッチして表示する、という一般的な機能の実現は可能なのですが、さらに踏み込んで「フェッチしたデータを元に、その値をユーザーが変更し、最後にその変更を DB に反映させる」という実装パターンを紹介します。このパターンを fetch and update パターンと呼ぶこととします。
今回の実装も DB Layer は Hasura を使っています
GraphQL を使うにあたって、迅速堅牢なアプリケーション開発が可能な Hasura を今回の実装でも使用しています。勉強会、公式ドキュメントの翻訳もおこなっていますのでこちらも参照ください。
fetch and update パターン
具体的によくあるケースとしては、ユーザーのプロフィールコメントをデータベースから取得し、input 要素に表示。ユーザーが input 要素にアクションをし文言を変更すると、まずはクライアント側の表示だけを変更する。そして最後に保存ボタンを押した時に、その変更をデータベースに反映させる。という機能です。
おそらくこれが React の状態管理としては一番複雑なケースだといっていいと思います。ですので fetch and update パターンを実装できるようになると、大抵の状態管理は実装できるようになります。
なぜ fetch and update パターンが難しいか
fetch and update パターンは一見するとよくある実装であり、何も難しいところがないように思われますが、実装してみるとなかなかうまくいかないことがわかります。その原因の一つは、React Apollo Hooks の onCompleted
が期待した動作をしない点にあります。
以下のコードの onCompleted
は、React Aollo Hooks を用いたデータフェッチが終了した後に実行するコールバックを指定する機能なのですが、これは refetch
時にはどうやら発動しないようなのです。(これが本来の挙動なのか、バグなのか、私の実装ミスなのかは定かではないが、少なくとも GitHub issue で同様の問題にぶつかっている人がかなりいます。)そのため、データベース側の値を更新した後に、最新の値を refetch で再取得しても、onCompleted
が発動しないために、useReducer で管理する値が更新されないのです。
export const Provider: FC = (props) => {
const { children } = props;
// ここでデータをフェッチ
const { data, refetch } = useUserInfoQuery({
variables: { userId },
onCompleted({ user_by_pk }) {
// ここで状態をセットすればいいと思いきや、
// 最初の1回目のフェッチ完了時には時には実行されるが、
// refetch 時には実行されないので問題がある
},
});
// ...
};
ではどうするか:Setter Layer を配置する
状態管理のためのコンポーネント構造概念図
Setter Layer を足したのが特徴。
実際の構造を実装したコードは以下のようになる
Setter Layer のコード
Context 内に保持している fetched data
が変更された時のみ、useEffect 内で setState
を実行することで、状態を変更する。
Provider Layer の役割:context を provide する
Provider layer の役割は、React.createContext が中心となって Provider 以下の Layer に対して context
を提供することです。context
を通して、useReducer を中心とした状態と状態の変更メソッドが配下の Layer に提供されます。
また、このレイヤーで React Apollo Hooks を使ってプロフィール情報をフェッチし、Provider に流しています。
必ずしもこのページに関連する全ての状態をこの Provider で管理をする必要はありませんが、このページ全体に関わる値はここで管理することで、コントロールが容易になります。
state は useReducer が主となって担う
Provider layer を通して渡される値のうち、状態管理を担う state とそれを更新する setState は、useReducer がその責務を主に担います。特に useReducer が使用する reducer にロジックが集中しているわけですが、このメソッドは別ファイル /Profile/reducer
に切り分けています。このファイルに、state, state を更新するための Action, そして更新ロジックが記された reducer が配置されています。
Setter Layer の役割
繰り返しになりますが、Setter Layer の役割は、Context 内に保持している fetched data
が変更された時に、useEffect 内で setState
を実行することで、状態を変更することです。
念のため申し上げておくと、Provider Layer で setState してしまうとコンポーネントの再更新が実行されてしまうため、無限に再レンダリングされてしまいます。(もし Provider 層でもうまく実装できるよという方がいれば、プルリクください)そのためこの Setter Layer を設けるパターンとなりました。
input value の変更をブラウザ側に反映させるコンポーネントの実装
useContextHook
を通じて、state
とそれを変更する setState
にアクセスします。
state
からプロフィールコメントに相当する値を input に入力し、変更があった場合には setState
を用いて変更します。
DB 側に変更リクエストを出すコンポーネントの実装
mutation を使って DB 側に変更を加えたら、refecth を使って更新します。
refetch によってデータを再取得すると fetchedData が変化するので、Setter Component 内の useEffect がそれを感知して、setState を行い、このページ内の情報が全て正しく更新されます。
TypeScript 的な注目点
以下実装参照ください。
- React Apollo Hooks で取得したデータにどのような型を与えればいいか
- Provider の受け取る value の 型をどう定義すればいいか
createContext
の初期値を、安全に運用するにはどうすればいいか