Slide 1

Slide 1 text

React への依存を最小にする フロントエンド設計 株式会社一休 CTO 室 恩田 崇

Slide 2

Slide 2 text

2 自己紹介 株式会社一休 CTO 室 恩田 崇 1978 年生まれ、京都在住 フルスタック、なんでも屋 一休レストランのフロントエンドアーキテクト Next.js App Router を Remix に書き換えた フロントエンドとは IE4/DHTML あたりから

Slide 3

Slide 3 text

3 依存が最小になっているとは? Remix の様々な adapter @remix-run/express @remix-run/cloudflare-workers @fastly/remix-server-adapter @vercel/remix Hono Cloudflare, Fastly, Deno, Bun, AWS, Node.js 昨日 remix-hono-adapter が Node.js 対応した フレームワーク自体が好例 ` ` ` ` ` ` ` `

Slide 4

Slide 4 text

4 依存が最小になっているとは? Vanilla JS で書かれた部分の多さ → React なしで使えるコードがどれだけあるか → testing-library なしでテストが書ける部分がどれだけあるか 一休レストランでは testing-library を使っていない 今日の話における定義、スコープを共有

Slide 5

Slide 5 text

5 目次 なぜ依存を最小にするのか? 技術選定 設計 一休レストランでの実践 今日お話しすること

Slide 6

Slide 6 text

6 なぜ依存を最小にするのか? エコシステムの変化に追随する負担を減らしたい フレームワークを切り替えたい よりよい設計を求めて 複数の動機

Slide 7

Slide 7 text

7 なぜ依存を最小にするのか? 一休レストランは2006 年から プロダクトはフレームワークより長寿命になることがある 2006 年というと… スマートフォンはまだない Chrome もない (2008 年) jQuery 1.0 がリリース 近日中に確実に来るメジャーバージョンアップ React 19 React Router v7 (f.k.a. Remix v3) エコシステムの変化に追随する負担を減らしたい

Slide 8

Slide 8 text

8 なぜ依存を最小にするのか? 2022 年末にリニューアルプロジェクト開始 旧版は Vue 2 / Nuxt 2 TypeScript で書かれている 旧版のコードをそのまま持ってこれない Vue / Nuxt を駆使している → 言い換えれば Vue / Nuxt にがっつり依存 Remix への移行 2023 年10 月 Next.js App Router でリリース 2023 年12 月 Remix に切り替えた フレームワークを切り替えたい

Slide 9

Slide 9 text

9 なぜ依存を最小にするのか? 初期に React 初心者がやらかしがちなコードが量産された 大きなコンポーネント コンポーネントにロジックが詰まっている 大量の useState 大量の useEffect 見通しが悪くコードの理解が難しい テストを書くのが困難 よりよい設計を求めて

Slide 10

Slide 10 text

10 目次 なぜ依存を最小にするのか? 技術選定 設計 一休レストランでの実践 今日お話しすること

Slide 11

Slide 11 text

11 技術選定 React に依存しないライブラリ 薄いフレームワーク 依存を最小化するにも基盤が必要

Slide 12

Slide 12 text

12 技術選定 swr → TanStack Query Recoil → Jotai XState ( 自作に置き換え中) いずれも Vanilla JS で使えるライブラリ React に依存しないライブラリ

Slide 13

Slide 13 text

13 技術選定 規約より API 規約への依存は見えない ファイル名、ディレクトリ構造 変数や関数名 明示的な API は参照箇所を追える React Router v7 から config base routing が! 標準 API の尊重 独自 API を使うところは少ない方が嬉しい 今だと Remix や Hono エスケープハッチ フレームワークの敷いてくれたレールを外れたいときがある 薄いフレームワーク

Slide 14

Slide 14 text

14 目次 なぜ依存を最小にするのか? 技術選定 設計 一休レストランでの実践 今日お話しすること

Slide 15

Slide 15 text

15 設計 依存を最小化するという考え方が古くからある オブジェクト指向とその爆発的な流行 テスト駆動開発 バックエンドの知見に倣う

Slide 16

Slide 16 text

16 設計 依存性逆転の原則 (Dependency Inversion Principle) 腐敗防止層 (Anti Corruption Layer) バックエンドの知見に倣う

Slide 17

Slide 17 text

17 設計 依存性逆転の原則 (Dependency Inversion Principle)

Slide 18

Slide 18 text

18 設計 腐敗防止層 (Anti Corruption Layer)

Slide 19

Slide 19 text

19 目次 なぜ依存を最小にするのか? 技術選定 設計 一休レストランでの実践 今日お話しすること

Slide 20

Slide 20 text

20 一休レストランでの実践 hooks にできるかぎりロジックを書かない export function useEventNavigate() { return useSetAtom(eventNavigate$) } const eventNavigate$ = atom(null, async (get, set, event) => { const env = get(env$) const navigate = get(navigate$) const { pathname, search } = get(location$) const current = get(historyState$) // snip })

Slide 21

Slide 21 text

21 一休レストランでの実践 コンポーネントは薄く小さく const styles = sva({ /* snip */ }) function IkyuPoint() { const { totalPoint, enabled, onChange } = useIkuPoint() const classes = styles() if (totalPoint === 0) { return (

保有ポイント

保有ポイントはありません

) } // snip }

Slide 22

Slide 22 text

22 一休レストランでの実践 Jotai で関数を管理することで簡易的な DI コンテナとしても利用 依存性逆転の原則と Dependency Injection function functionAtom( fn: F ): WritableAtom { const wrapper = atom({ fn }) return atom( (get) => get(wrapper).fn, (_get, set, fn) => { set(wrapper, { fn }) } ) }

Slide 23

Slide 23 text

23 一休レストランでの実践 Date を使わず日付や時刻を表す型を作成 graphql-codegen で Custom Scalar として利用 Temporal がいずれは base line に Java はかつて java.util.Date から Date-Time API に移行した 腐敗防止層 const zDateText = z .string() .regex(/^\d{4}-\d{2}-\d{2}$/) .brand('DateText') type DateText = z.infer // HourMinute, DateTime など ` `

Slide 24

Slide 24 text

24 一休レストランでの実践 test('日付変更で、選択されている時間帯にもっとも近い予約可能な時間を設定', async () => { const fetchTimes = vi.fn().mockResolvedValue({ /* snip */ }) const { transition } = createStateMachine({ fetchTimes }) const current = createCurrent() const result = await transition( current, calendarEvent('selectVisitDate', { visitDate: '2024-10-26' as DateText }) ) expect(result.value).toEqual('READY') expect(result.context).toEqual({ ...current.context, visitDate: '2024-10-26', selectedVisitDate: '2024-10-26', visitTime: '18:30', }) })

Slide 25

Slide 25 text

25 エンジニア募集中! 一休では、よりよいサービスを届ける仲間を募集しています。