実装コード: 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 を足したのが特徴。

Screen Shot 2020-09-25 at 16.37.54

実際の構造を実装したコードは以下のようになる

Screen Shot 2020-09-25 at 16.37.26

Setter Layer のコード

Context 内に保持している fetched data が変更された時のみ、useEffect 内で setState を実行することで、状態を変更する。

Screen Shot 2020-09-25 at 16.35.39

Provider Layer の役割:context を provide する

Provider Layer の実装コード

Provider layer の役割は、React.createContext が中心となって Provider 以下の Layer に対して context を提供することです。context を通して、useReducer を中心とした状態と状態の変更メソッドが配下の Layer に提供されます。

また、このレイヤーで React Apollo Hooks を使ってプロフィール情報をフェッチし、Provider に流しています。

必ずしもこのページに関連する全ての状態をこの Provider で管理をする必要はありませんが、このページ全体に関わる値はここで管理することで、コントロールが容易になります。

state は useReducer が主となって担う

/Profile/reducer/index.ts の実装

Provider layer を通して渡される値のうち、状態管理を担う state とそれを更新する setState は、useReducer がその責務を主に担います。特に useReducer が使用する reducer にロジックが集中しているわけですが、このメソッドは別ファイル /Profile/reducer に切り分けています。このファイルに、state, state を更新するための Action, そして更新ロジックが記された reducer が配置されています。

Setter Layer の役割

繰り返しになりますが、Setter Layer の役割は、Context 内に保持している fetched data が変更された時に、useEffect 内で setState を実行することで、状態を変更することです。

Screen Shot 2020-09-25 at 16.35.39

念のため申し上げておくと、Provider Layer で setState してしまうとコンポーネントの再更新が実行されてしまうため、無限に再レンダリングされてしまいます。(もし Provider 層でもうまく実装できるよという方がいれば、プルリクください)そのためこの Setter Layer を設けるパターンとなりました。

input value の変更をブラウザ側に反映させるコンポーネントの実装

useContextHook を通じて、state とそれを変更する setState にアクセスします。

state からプロフィールコメントに相当する値を input に入力し、変更があった場合には setState を用いて変更します。

Screen Shot 2020-09-25 at 16.59.51

DB 側に変更リクエストを出すコンポーネントの実装

mutation を使って DB 側に変更を加えたら、refecth を使って更新します。

Screen Shot 2020-09-25 at 17.03.29

refetch によってデータを再取得すると fetchedData が変化するので、Setter Component 内の useEffect がそれを感知して、setState を行い、このページ内の情報が全て正しく更新されます。

TypeScript 的な注目点

以下実装参照ください。

  • React Apollo Hooks で取得したデータにどのような型を与えればいいか
  • Provider の受け取る value の 型をどう定義すればいいか
  • createContext の初期値を、安全に運用するにはどうすればいいか

Screen Shot 2020-09-25 at 17.07.32