Upgrade to Pro — share decks privately, control downloads, hide ads and more …

複雑なフォームを継続的に開発していくための技術選定・設計・実装 #tskaigi / #tsk...

複雑なフォームを継続的に開発していくための技術選定・設計・実装 #tskaigi / #tskaigi2025

Avatar for Masayuki Izumi

Masayuki Izumi

May 23, 2025
Tweet

More Decks by Masayuki Izumi

Other Decks in Programming

Transcript

  1. @izumin5210 © LayerX Inc. whoami LayerX バクラク事業部 (2022-09 -) Platform

    Engineering 部 Enabling チーム Staff Software Engineer ISUCON14 4位 好きな関数は cva
  2. 最も単純な実装 © LayerX Inc. useState だけ この時点で将来的に困る気配がする フィールドが増えると useState も増える

    1フィールド更新しただけで全体再描画 バリデーションどうする? これ以上複雑にならないなら全然これでもいい 7
  3. フォームライブラリで便利に コード例は React Hook Form © LayerX Inc. 基礎的な面倒事は吸収してくれる useForm

    して <input/> を register する いいかんじに再描画を減らす バリデーションの実行と制御もやってくれる フィールドが多少増えた程度なら、 これだけでも戦える ...本当に? 8
  4. フォームライブラリ + バリデーションスキーマ コード例は React Hook Form + Zod ©

    LayerX Inc. フォームが扱う値の構造と制約を 宣言的に記述できる(させる) 重要なロジックや構造を UI から 独立して扱える バリデーションのついでにフォームのモデリング を開発者に促す 10
  5. フォームライブラリ + バリデーションスキーマの限界 © LayerX Inc. React Hook Form +

    Zod でいうと、 watch や setValue が増えだしたらアラート 多少ならまだいいが、 useEffect とかと組み合わさり始めるとかなり危険信号 危険なサインの例 useEffect or watch で setValue を手続き的に呼び出すことによるバグ混入リスク イベントハンドラ内であっても、フォーム状態を無秩序に setValue することによる、 重要な書き込みロジックが無視されるリスク モデルの振る舞いをコードに反映できてないサインかも 18
  6. フォームライブラリ + バリデーションスキーマの限界 © LayerX Inc. 参考: useEffect + setState

    は危険信号 そのエフェクトは不要かも – React https://ja.react.dev/learn/you-might-not-need-an-effect 詳解:フロントエンドの状態とリアクティブ (なぜuseEffect()でsetState()がアンチパターンか) https://zenn.dev/layerx/articles/22dd45dc69a57c 19
  7. フォームライブラリ + バリデーションスキーマの限界 © LayerX Inc. Zod でモデリングできるのはあくまで 「形状」と「制約」 だけ

    それ以上の、たとえば今回のような計算・ロジックは組み込みづらい (頑張ればできるが、Zod の宣言的に書ける良さは活きづらい) 20
  8. 適切なモデルを考える © LayerX Inc. 今回の例(小計・合計)は、 「ある値から別の値を計算し、表示やバリデー ションに使いたい」というもの RHF + Zod

    の都合に合わせるために、計算結果を状態に書く 単純なプログラムなら「一度状態に持つ」とい うことはしないのでは? 状態にせず値としたい 21
  9. 適切なモデルを支える技術選定例 © LayerX Inc. オブジェクト指向のようなモデルがフィットし ているなら、MobX が選択肢に上がるかも データの依存を素直に記述でき、 それを効率よく React

    に伝えてくれる 実行時にデータの依存(今回だと getter が何を読んだか)を追跡 し、更新検知・再描画をしてくれる とはいえ MobX は癖があるし、OOP が React を触るエンジニア にとって扱いやすいかなど、別角度での検討事項は多い 22
  10. ポイント① モデルは「形状」だけでなく、ロジック・振る舞いも含める © LayerX Inc. Zod だと「形状」 「制約」にとどまることが多い が、実際のプロダクトの複雑なフォームでは、 そこ以外にも重要な概念がある

    「合計金額は数量と金額の掛け算」とかはシンプルだが、 現実はもっと難しいはず そういうドメイン上重要なものに名前をつけて、 重要だとわかるようにしたい 24
  11. ポイント② フォームにおけるモデルを UI から分離する © LayerX Inc. React Hook Form

    + Zod だと厳しくなってくるようなフォームでは、 価値の高い・重要なロジックが埋まっていることも多いはず それらを UI から分離し「 (重要な)モデルである」というのを わかりやすく・見通しやすく・テストしやすくしておきたい そこをより重点的にテストする, そこはより注意してレビューする, ... 開発者の認知や行動を切り替える (とはいえ規模や複雑さがそうでもないなら UI に co-locate させるほうがメンテしやすい。要はバランス。 ) AI 向けにもモデルと UI が分離されたほうが動きやすいし、我々ももレビューしやすい…んじゃないかな? 25
  12. jotai の簡単な例 © LayerX Inc. jotai は atom という単位で状態を管理する atom

    関数は読み書き可能な状態のほか、 Read only な atom をつくることで、 他の値に依存した値(Derived Value)を 表現可能 この get(anAtom) の形でデータの依存グラフを描く 30
  13. jotai で非同期の Derived Value を扱う例 © LayerX Inc. Read only

    atom は非同期の値を返す こともできる 非同期 atom もだいたい通常の atom と同じように扱える 解説サボるために実装を簡略化しています。実際の利用方法は以下の記事を参照してください 「Jotai v2を使いこなすために実は必須級な“async sometimes”パターンの解説」 by uhyo https://zenn.dev/uhyo/articles/jotai-v2-async-sometimes 31
  14. jotai によるモデリング © LayerX Inc. MobX のときに挙げたポイントは jotai もクリアしている ポイント①

    モデルは「形状」だけでなく、ロジック・振る舞いも含める ポイント② フォームにおけるモデルを UI から分離する ポイント③ なるべく状態ではなく、値として扱う やはり「ロジックを」 「UI と分離して」 「値でいいものは値として扱う」ことで 余計な複雑性を抑えたモデリングができる 33
  15. jotai によるモデリング - 非同期処理 © LayerX Inc. 非同期処理を含む atom への依存をそこまで特別扱いせず、

    おなじく atom の形で書けるのが強力 前述のレートの例のような「ユーザの入力値を API に渡し、 そのレスポンスを別の値やバリデーションに利用」は稀によくあるケース 非同期処理のために処理の流れを捻じ曲げたり、余計な中間状態を生やしたりは不要。 いつもの async function で OK。 React にマウントするときにまだ未解決なら勝手に Suspend してくれる 便利! …だけど、同じ useAtomValue が atom によって suspend したりしなかったりするのは 逆に認知負荷たかいかも…? ともちょっと思う。むずかしい。 34
  16. jotai によるモデリング - テスト © LayerX Inc. コアは React にすら依存してないので、テストも書きやすい

    API に依存してる場合はモックに差しか可能にするか、 MSW で外側からモックするか 35
  17. MobX と jotai - どっちにするか © LayerX Inc. 依存追跡の方法が大きく違う MobX

    はランタイムで暗黙的に呼び出しを記憶 jotai は get(anAtom) の呼び出しをリアクティブな(更新されうる)値の呼び出しとする get(anAtom) の記述など、独自 DSL 感のある jotai か 普通の OOP / クラスっぽくかける代わりに、良くも悪くも暗黙的な挙動が多い MobX か 37
  18. 問題によって適するモデル・設計は変わる モデル・設計が変われば適する技術も変わる © LayerX Inc. 今回は偶然 Derived Value / データの依存グラフ

    / データの流れ などに着目した設計が ハマる問題(プロダクト特性)だった 問題(プロダクトや機能)によって適する回答は変わりうる ウィザードっぽい複数画面からなるフォームであれば、 XState のような状態遷移に着目した技術が適するかも Undo 機能みたいなものが必要なら Redux / Zustand などが適するかも 単にフィールドが多いだけのフォームなら、 Zod で「形状」をモデリングするだけで事足りるかも プロダクトの複雑さはどこから来ているかを見極める 39
  19. フォームライブラリを使わないことで失うものもある © LayerX Inc. これもトレードオフのひとつ たとえば React Hook Form であれば、

    各フィールドの dirty check, バリデーション発火タイミング制御, etc. なくなったら面倒ではあるが、 「本質的な複雑さ」というよりは「シンプルに面倒」な類のものも多い なので最小限のユーティリティを実装してなんとかするという手もある 41
  20. 複数の技術・設計を組み合わせるパターンもありうる © LayerX Inc. たとえば以下の2つは両立しうる 「バリデーション対象フィールドが多いので Zod で宣言的 に記述したい」 「依存関係を持つフィールドが多いので

    jotai を使いたい」 Zod のバリデーション結果も Derived Value にしてしまえ ばいい jotai と Zod を組み合わせると、 superRefine などは不要になるはず 42
  21. 最初から100点の答えを見つけに行く必要はない © LayerX Inc. 特に PMF 前のプロダクトや機能のプロトタイプフェーズなどでは、 あるべきの形が見えてないことも多い そんな状態で未来を先読みするのは難しいし打率も低い なので、そこそこいい感じになるところから始めよう

    やはりフォームライブラリ(e.g. React Hook Form)とバリデーションスキーマ(e.g. Zod)の 組み合わせから入るのはコスパがいい まあこの2つで事足りるケースのほうが多いだろうし… 43
  22. 「変更影響を受けた(壊れた)こと」に早く気付けるコードを書く © LayerX Inc. 気づくスピード: 型 > Lint > Unit

    Test > ... > エンドユーザ 特に規模が大きい/複雑/パターン数が多い部品では なるべく型やテストが最初に壊れるようにしておく 現状の仕様をモデル+テストで表現できているのであれば 型で将来的な拡張への耐性を上げることが有効 45
  23. 早く壊れる型の例. などなど… © LayerX Inc. enum, union で取りうる値の幅が増えてハンドル漏れが起きるケース foo satisfies

    never のような Exhaustiveness checking を常に書く 渡せるパラメタが増えたが、呼び出し側でその対応が漏れるケース アプリケーションコードでは optional は使わない(不要なら null などを明示的に渡させる) 変更が伝播し、意図せず Atom の型(関数の返り値)が変わって壊れる 関数の返り値型は推論に頼らず明示する 46