Jotaiで作ったフォームをApollo Clientで投げたらいい感じだった件Asazu Taiga @JADE K.K. 2023.3.21 Saitama.js@大宮【React】
View Slide
Saitama.js オフライン開催おめでとうございます!あーしもよろこんでいます※公式の利用ルールに則り、非営利目的にて春日部つむぎさん素材を利用させていただいております
タイトルに「いい感じ」とありますが、本当にいい感じなのか?は皆さんの目でお確かめください(登壇申し込み時点ではいい感じだと思ってた)
自己紹介● Asazu Taiga● twitter○ @AsazuTaiga● 所属○ 株式会社JADE● 最近好きなもの○ VTuberの音楽○ ボイロキッチン動画■ (自炊しないのに)
Agenda
Agenda● Jotaiの紹介● Apollo Clientの紹介● GraphQL Code Generatorの紹介● Jotaiを使ったフォームの作成● デモ● ちょっといい感じポイント● ちょっとどうかなポイント● まとめ● 質疑応答
Jotaiの紹介
Jotaiとは?● グローバルな状態管理ライブラリ● Recoilに触発されたアトミックなアプローチ● アトムの組み合わせによる依存関係に基づく自動最適化○ 再レンダリング問題の解決、メモ化が不要● シンプルなuseStateの置き換えから複雑なアプリケーションまで幅広く対応○ ユーティリティも豊富にある
Jotai サンプル(公式から引用)import { atom, useAtom } from 'jotai'// Create your atoms and derivativesconst textAtom = atom('hello')const uppercaseAtom = atom((get) => get(textAtom).toUpperCase())// Use them anywhere in your appconst Input = () => {const [text, setText] = useAtom(textAtom)const handleChange = (e) => setText(e.target.value)return ()}
Jotai サンプル(公式から引用)const Uppercase = () => {const [uppercase] = useAtom(uppercaseAtom)return (Uppercase: {uppercase})}// Now you have the componentsconst App = () => {return (<>>)}
Apollo Clientの紹介
Apollo Clientとは?● JavaScript向けの包括的な状態管理ライブラリ○ GraphQLクライアント○ ローカルとリモートの両方のデータを管理● データの取得、キャッシュ、変更を行い、UIを自動的に更新● GraphQL Code Generatorと組み合わせることで、TypeScriptとの統合がより便利になる
Apollo Client サンプル(公式から引用)const GET_LOCATIONS = gql`query GetLocations {locations {idnamedescriptionphoto}}`;
Apollo Client サンプル(公式から引用、一部略)function DisplayLocations() {const { loading, error, data } = useQuery(GET_LOCATIONS);if (loading) return Loading...;if (error) return Error : {error.message};return data.locations.map(({ id, name, description, photo }) => ((中略...)));}
GraphQL Code Generatorの紹介
GraphQL Code Generatorの雰囲気が伝わる図.graphql ドキュメント.graphqls スキーマ@graphql-codegen/cli各種プラグイン(TypeScriptの型定義生成、Apolloのhooksのラッパー生成etc…)generated.ts
Jotaiを使ったフォームの作成
Jotaiを使ったフォームの作成※jotai-formというatomWithValidate()を提供する utilライブラリもありますが、今回は使いません理由● Jotai本体だけで十分に柔軟な表現ができる(気がする)● validate()関数をアトムに追加できるだけなので恩恵がさほど大きくない(気がする)
Jotaiを使ったフォームの作成 - nameフィールドの例// エラーメッセージを表示するかどうかを判定するためのatom(フォーム全体で共有)const shouldShowErrorMessageAtom = atom(false);const nameAtom = atom("");const nameSchema = z.string().min(3, "名前は3文字以上で入力してください").max(20, "名前は20文字以内で入力してください");
Jotaiを使ったフォームの作成 - nameフィールドの例const nameErrorAtom = atom((get) => {const name = get(nameAtom);const result = nameSchema.safeParse(name);if (result.success) {return "";}return result.error.issues[0].message;});const nameDisplayErrorAtom = atom((get) => {const shouldShow = get(shouldShowErrorMessageAtom);const error = get(nameErrorAtom);return shouldShow ? error : "";});export const useName = () => {const [value, setValue] = useAtom(nameAtom);const errorMessage = useAtomValue(nameDisplayErrorAtom);return { value, setValue, errorMessage };};
デモ
デモWebsitehttps://asazutaiga.github.io/jotai-examples/dist/index.htmlRepositoryhttps://github.com/asazutaiga/jotai-examples
ちょっといい感じポイント
ちょっといい感じポイント● クロスフィールドバリデーションが書きやすい● Mutation引数等、型変換をかませた派生atomが便利● 利用側のコードがシンプルになる● 状態依存部分を最小限に切り出したコンポーネントを書けば再レンダリングは控えめ
クロスフィールドバリデーションが書きやすいconst passwordErrorAtom = atom((get) => {const password = get(passwordAtom);// example: cross field validation// 実際こんなことはしないと思うが、例としてconst name = get(nameAtom);if (name === password) {return "名前とパスワードは異なるものにしてください ";}return "";});
Mutation引数等、型変換をかませた派生atomが便利// mutationの引数:GraphQL Code Generatorで生成した型を使うことができる// この例では単にそのまま値を各フィールドに渡しているだけだが、ここで適切な変換をかませることもできるconst createUserVariablesAtom = atom((get) => ({input: {name: get(nameAtom),email: get(emailAtom),password: get(passwordAtom),},}));export const useCreateUserVariables = () => useAtomValue(createUserVariablesAtom);
Mutation引数等、型変換をかませた派生atomが便利// 使う側はこんなにシンプル!// (いっそこれらをまとめたhookを作ってしまうのもよいが、ややオーバーか?)const variables = useCreateUserVariables();const [createUser, { loading }] = useCreateUserMutation({variables,});
利用側のコードがシンプルになるconst NameInput = () => {const { value, setValue, errorMessage } = useName();return (label="Name"value={value}onChange={(e) => setValue(e.target.value)}/>{errorMessage});};const InputView = () => {return ( e.preventDefault()}>);};
状態依存部分を最小限に切り出したコンポーネントを書けば再レンダリングは控えめデモで見てもらったように、フォーム全体の再レンダリングはリセット時やモード(入力or確認)の切り替え時にしかおきません
ちょっとどうかなポイント
ちょっとどうかなポイント● テンプレ的な記述を多く書くのが大変● フィールドが増えた際に依存フィールドの修正もれそう● グローバルで命名の衝突を避けようとすると変数名が長くなりがち
テンプレ的な記述を多く書くのが大変すでに見てもらったように、一つのフィールドに対して● 値atom● 内部エラーatom● 表示用エラーatomの3つは最低でも定義したくなるフィールドが増えれば、それだけatomの管理も大変に...
フィールドが増えた際に依存フィールドの修正もれそう// フォームの入力値をリセットするためのatomconst resetFormAtom = atom(null, (get, set) => {set(nameAtom, "");set(emailAtom, "");set(passwordAtom, "");set(shouldShowErrorMessageAtom, false);});addressが増えたらここに足す...
フィールドが増えた際に依存フィールドの修正もれそう// フォーム全体をまたいで、エラーがあるかかないかを判定するためのatomconst hasErrorAtom = atom((get) => {const nameError = get(nameErrorAtom);const emailError = get(emailErrorAtom);const passwordError = get(passwordErrorAtom);return nameError || emailError || passwordError;}); ここもやんけ...
グローバルで命名の衝突を避けようとするとatom名が長くなりがち● フォームの種別+フィールド名(+小区分)+atom○ createUserNameAtom○ createUserNameErrorAtom○ createUserNameDisplayErrorAtom
まとめ
まとめ● 派生状態を作りやすいJotai(atomベースの状態管理)の特徴が、フォームの状態管理と結構マッチしている?● Apolloのことを散々書きましたが、APIのInputの型があればGraphQLでもRESTでも関係ないですね(今更)● フォームの状態管理はみんな苦しんでいると思うので、ひとつのありうる選択肢として捉えるとよさそう
Special Thanks春日部つむぎさん(かわいい)Chat GPT(スライド作成手伝ってくれた)
質疑応答