「Redux よさようなら」最強の React 実装
Tweet- useState, useContext を使った状態共有
- graphQL BFF + Codegen + Apollo Hooks で型安全なデータ取得
- Redux の型定義、Loading 状態の運用、エラー処理、等々煩雑なコードがなくなる
1日一つ強くなる中西とは
- https://ja.reactjs.org/ React 日本語公式ドキュメントのトップページの翻訳を担当
- Apollo Japan User Group 主宰 / 公式ドキュメントチュートリアルを翻訳しました
- Hasura Japan User Group 主宰 / チュートリアル公開しています
- JavaScript が中心となった生産性の高い Web App 開発チームの運用、及びその普及活動が今のテーマ => https://apollographql-jp.com/makeMoneyTS/
- 毎月 JavaScript を中心とした勉強会を主宰していますので是非参加してください。(React, TypeScript, React Native, Apollo, Hasura)6月は Hasura Japana User Group # 2 です。
最強とは何か
今日、昨日よりも一つ強くなる。それが死ぬまで続けば、「最強」だ。と、格闘ゲーム「ストリートファイター」の伝説、梅原大吾氏が言っていた。「Web Developer」を目指し、1日ひとつ強くなる日々の記録である。
これは私のプロフィールですが、私の考える最強とは上記の通りです。React に即して言えば、とにかく毎日仕事でコードを書き、課題に取り組みながら、それを解決するための実装を毎日改善していく中でたどり着いたコードこそが、最強のコードであり、それを続ける限り、明日も明後日もより最強になっていきます。
そういう意味では今日ご紹介する実装よりも、明日はもっと強い実装をしていると思いますが、なによりも重要なのは、日々戦ってきた中で勝負に勝ってきたコードだということです。
実装もせずに、想像上の理論的なベストプラクティスを紹介をすることは私はありません。日々 JavaScript を書き、困難に対峙するエンジニアのプラスになることを目的にこの勉強会、そしてブログ等を書いています。
同じ志をもった JavaScript エンジニアの勉強会での発表を期待します。
Redux の欠点 1: 必要なければ必要ない
前提としてどのような Tool を使うかはエンジニアが決めることであって他人がそれについて可否を判断する権限はない。しかし共通のコンテクストのもと、検討していくことはできる。共通のコンテクストとはつまり、コンポーネント間を超えた状態共有の必要性をどのように我々は解決すればいいのか、ということである。
コンポーネントを超えた状態共有するために、以前は Redux が事実上のデファクトスタンダードであった。Redux が必要とされる状況にあったのは、何よりも React 本体に「コンポーネント間を超えた状態共有」を行う機能が React の標準ライブラリでは提供されていなかったという理由が大きい。React の機能だけでおこなうという選択肢がそもそもなかったのである。
しかしコンポーネント間の共有は「useState(or useReducer)」と「reactContext(createContext + useContext etc.)」によって実現可能になった。
Redux が useState + useContext 以上の、我々が必要とする機能を提供することがない
少なくとも私が日々実務で実装する中では Redux に戻らなければ実装できないケースが発生しなかった。
useState + useContext パターンに不足があれば、Redux に戻っているはずだが、そういった機会が発生しなかった。もちろんこれは GraphQL による BFF, Apollo Hooks 等を活用することによって解決している部分があり、これらを使わない場合には発生するかもしれない。
また、もし useState + useContext パターンに不足があれば是非ご連絡いただきたい。ウェブ開発のベストプラクティスに乗っ取っている限りは、解決策を提示できるはずだ。
useState + useContext パターンのコード
サンプルコード
https://codesandbox.io/s/reactcontext-example-57y7p?file=/src/App.tsx
import * as React from "react";
type State = number[];
type Action = { index: number };
const myReducer = (state: State, action: Action) => {
return state.filter((_, i) => i !== action.index);
};
type AppContextType = {
items: number[];
deleteItem: React.Dispatch<Action>;
};
export const AppContext = React.createContext<AppContextType>({
items: [],
deleteItem: () => {}
});
export const useAppContext = () => React.useContext(AppContext);
export const AppContextProvider: React.FC = ({ children }) => {
const [items, deleteItem] = React.useReducer(myReducer, [
0,
1,
2,
3,
4,
5,
6,
7
]);
return (
<AppContext.Provider value={{ deleteItem, items }}>
{children}
</AppContext.Provider>
);
};
Provider で state, setState を流し込む
AppProvider.tsx
内で AppContextProvider
コンポーネントを定義し、これを使って state とそれを変更する関数 setState を流し込む。<AppContextProvider />
でラップした内部で useContext が使用可能になる。
つまり、状態関連のロジックを AppProvider.tsx
に全て閉じることが(理想的には)可能になる。これ以外のコンポーネントを状態のロジックから切り離すことが可能。
// AppProvider.tsx
export const AppContextProvider: React.FC = ({ children }) => {
const [items, deleteItem] = React.useReducer(myReducer, [
0,
1,
2,
3,
4,
5,
6,
7
]);
return (
<AppContext.Provider value={{ deleteItem, items }}>
{children}
</AppContext.Provider>
);
};
// App.tsx
import { AppContextProvider } from "src/AppProvider";
export const App = () => {
return (
<AppContextProvider>
<Title title="this is title" />
<List />
</AppContextProvider>
);
};
useReducer で setState 関連のロジックを閉じ込める
deleteItem メソッドは、配列のうち該当する index の item を削除するメソッドであるが、こういったロジックをどこに書くかをかなり悩んできた。結論としては useReducer 内にロジックを保持するパターンが、一番疎結合である。
type State = number[];
type Action = { index: number };
const myReducer = (state: State, action: Action) => {
return state.filter((_, i) => i !== action.index);
};
export const AppContextProvider: React.FC = ({ children }) => {
const [items, deleteItem] = React.useReducer(myReducer, [
0,
1,
2,
3,
4,
5,
6,
7
]);
return (
<AppContext.Provider value={{ deleteItem, items }}>
{children}
</AppContext.Provider>
);
};
通常、reducer は action.type
をもとに条件分岐することを想定しているが、上記のコードのように action.type
で分岐をしなくとも全く問題ない。ロジックを閉じるための場所として使用可能である。
useContext の使用
<AppContextProvider />
によって注入された値は、以下のように useContext を用いてアクセスする。
import React from "react";
import { useAppContext } from "src/AppProvider";
type Props = {
data: number;
index: number;
};
export const Item = (props: Props) => {
const { deleteItem } = useAppContext();
const { data, index } = props;
const clickHandler = () => deleteItem({ index });
return (
<div onClick={clickHandler}>
Item data: {data} index: {index}
</div>
);
};
Redux の欠点 2: 型定義をたくさん書く、かつだんだん分からなくなってくる気がする…
欠点として指摘しておきながら、ふわっとしていて大変申し訳ないが、Redux 時代には型定義のためにかなりたくさんのコードを書き、そしてだんだん途中から何を書いているのか分からなくなっていた。
useState + useContext パターンにおいても、型定義は一個一個書くしかないのだが、普通に書けば書ける。なぜあんなにも Redux 時代には苦労していたのかよくわからない。
もう少し明確にしたかったのだが、あんなに毎日書いていた Redux の書き方をほとんど忘れてしまったのと(それから redux-saga のことも)、useState + useContext パターンのベストプラクティスをお伝えする方が価値が高いと思ったので、あまり精度を上げなかった。
graphQL BFF + Codegen + Apollo Hooks で型安全なデータ取得
サンプルアプリのコード
https://github.com/superyusuke/react-typescript-apollo-bff-codegen
- 通常の RESTFulAPI の場合、レスポンスの型がわからない。一般的には Swagger 等の OpenAPI 規格でドキュメントを定義し、それを元に操作する => そこそこ面倒、かつ OpenAPI の仕様書を書かなくなると、終わる
- GraphQL の場合、Schema = どんなエンドポイント(的なもの)があって、そこからどのような型の値が返ってくるかを「まず」定義するので、上記のような問題は発生しない
- この GraphQL Schema を参照することで、Client 側はどんな値が返ってくるか明確になる
- さらに、Raect 側のコードを監視して、値を fetech するための hooks を自動生成してくれる
Demo: 1 実際のコードを見る
Demo:2 削除機能を追加する
- BFF に削除する Schema を設定する
- Schema に乗っ取って Resolver を実装する(Schema とあっていなければエラーが出る)
- Client 側で Schema を読みとった情報をもとに、GraphQL Query を書く
- 書いた Query を元に削除専用の hooks ができる
- この hooks を使って BFF と通信することで、削除する
非同期処理、ローディング状態の管理が異常に楽
- 非同期処理をするために redux-thunk or saga をインストールして、ロード前に loading を true にして、フェッチが終わったら、loading: false, 取得した値をセットするための action を発行して…という手順がなくなる
- その代わり、クライアントがやっていた非同期処理のチェーンを BFF に寄せる必要がある(それができない場合、hooks の生産性は最大限に発揮できない)