Slide 1

Slide 1 text

プロダクトを少人数でスケールさせてきた僕ら Rebase Tech #0 プロダクト改善のために新しいことを始める ~ useContextからの卒業、 zustandへ ~ 株式会社 Rebase @y-okai 1

Slide 2

Slide 2 text

2つ紹介したい ● Main ) 新しい技術を導入するのに必要な準備とは何か ● Sub ) useContextからどうやってzustandへ移行するか 今日のゴール 2

Slide 3

Slide 3 text

● TOIROについて ● プロダクトで生まれた課題と提案 ● zustandを導入してみて ● まとめ 目次 3

Slide 4

Slide 4 text

自己紹介 株式会社Rebase エンジニアリング @y-okai 経歴 ● 不動産テック企業 エンジニア・スクラムマスター ● 株式会社Rebase エンジニア 主な仕事 ● コミュニティイベントサービス「TOIRO」の開発全般を 担当 ● フルスタックには動いていますが、 どちらかというとフロントエンド寄り 4

Slide 5

Slide 5 text

5 TOIRO について

Slide 6

Slide 6 text

事業紹介 ・ イベントの作成 ・ 集客 ・ クレジットカード決済 ・ QRコード受付 ・ オープンチャット(TOIROG) TOIRO(https://toiro.com/) これまでにない新たなイベント体験を提供するコミュニティイベントサービス イベントを主催する方々にとって便利な機能を順次開発し実装していく予定です

Slide 7

Slide 7 text

使っている主な技術 ● フロントエンド ○ TypeScript ○ React.js ○ Next.js ○ TailwindCSS ● バックエンド ○ TypeScript ○ Node.js ○ GCP, Firebase ○ Terraform TOIROについて 7

Slide 8

Slide 8 text

8 プロダクトで生まれた 課題と提案

Slide 9

Slide 9 text

生まれた課題 前提 ● TOIROには「イベントを作成する」フォームがある ○ タイトル、詳細、日程、場所、公開設定、色設定、 etc. ● 入力内容によって、他のフォームの表示・非表示や必須項目が変わる 課題 ● 機能拡張するにつれて、状態管理が複雑化すること ● 状態変化による再レンダリング問題 プロダクトの中で生まれた課題と提案 9

Slide 10

Slide 10 text

プロダクトの中で生まれた課題と提案 10 サンプルを使って解説します

Slide 11

Slide 11 text

例) ToDoアプリの場合 プロダクトの中で生まれた課題と提案 11 (1)色の変更
 (2)タスクの追加
 (3)タスクの追加 (モーダル)
 (4)タスクの表示
 (5)タスクの保存


Slide 12

Slide 12 text

どのように実装していたか ● useContext + useReducerでページ全体の状態を管理 ○ アクション(色の変更、タスクの追加)ごとに状態を反映 ● 「一旦保存する」ボタンでDBへ保存 例) ToDoアプリの場合 プロダクトの中で生まれた課題と提案 12

Slide 13

Slide 13 text

実際に動かしてみると ● 選択対象以外で再レンダリングが起 こる... 例) ToDoアプリの場合 プロダクトの中で生まれた課題と提案 13

Slide 14

Slide 14 text

解決策を考える ● 案1) リファクタリングしてなんとかする ○ Contextを分割する ○ React.memoを利用する ○ useMemoを利用する ● 案2) 新しいツールを導入する プロダクトの中で生まれた課題と提案 14

Slide 15

Slide 15 text

解決策を考える ● 案1) リファクタリングしてなんとかする ○ Contextを分割する ○ React.memoを利用する ○ useMemoを利用する ● 案2) 新しいツールを導入する プロダクトの中で生まれた課題と提案 15 課題1) 
  管理が必要な状態が増える 
 課題2)
  対象箇所全てにメモ化処理が必要(開発者に依存) 


Slide 16

Slide 16 text

解決策を考える ● 案1) リファクタリングしてなんとかする ○ Contextを分割する ○ React.memoを利用する ○ useMemoを利用する ● 案2) 新しいツールを導入する(こっちで考える) プロダクトの中で生まれた課題と提案 16

Slide 17

Slide 17 text

プロダクトの中で生まれた課題と提案 17 新しいことを始めたい

Slide 18

Slide 18 text

プロダクトの中で生まれた課題と提案 18 新しいことを始めたい → 調査が必要

Slide 19

Slide 19 text

調査すること (1) ツールの選定 (2) 導入するタイミング (3) 見積もり時間 (4) コード変更の範囲 (5) チームの学習コスト 19 プロダクトの中で生まれた課題と提案

Slide 20

Slide 20 text

(1)ツールの選定 ● 候補 プロダクトの中で生まれた課題と提案 20 ライブラリ 記述量 Provider必須 設計の柔軟性 グローバル状態 ライブラリサイズ Zustand 少ない 不要 高い 可能 小さい Jotai 少ない 不要 高い 可能 小さい Redux Toolkit 多い 必要 標準的 可能 中〜大 ● 考慮したいこと ○ 機能拡張の増加(== 管理する状態が増える)に耐えられるもの ○ 学習コストを減らすため、記述量が少なくシンプルなもの

Slide 21

Slide 21 text

(1) Zustand vs Jotai プロダクトの中で生まれた課題と提案 21 Zustand 
 Jotai 
 const countAtom = atom(0) const textAtom = atom('') const flagAtom = atom(false) function SampleComponent() { const [count, setCount] = useAtom(countAtom) const [text, setText] = useAtom(textAtom) return (
{count}
setCount(c => c + 1)}>+1 setText(e.target.value)} />
) } const useStore = create((set) => ({ count: 0, text: '', flag: false, inc: () => set(s => ({ count: s.count + 1 })), setText: (text) => set(() => ({ text })), toggleFlag: () => set(s => ({ flag: !s.flag })), })) function SampleComponent() { const { count, text, inc, setText } = useStore(useShallow((state) => { count: state.count, text: state.text, inc: state.inc, setText: state.setText } )) return (
{count}
+1 setText(e.target.value)} />
) } ● ZustandはuseReducer、JotaiはuseStateに コード設計が近い ● Zustandはセレクタもstoreのなかで管理できる  → Zustandを選択

Slide 22

Slide 22 text

(2)導入するタイミング 新しい機能開発プロジェクトと同時に行う ○ 大規模なコードの変更が発生することになった ○ 細部までシナリオテストを実施することになり、degradeもここで防げる プロダクトの中で生まれた課題と提案 22

Slide 23

Slide 23 text

(3)見積もり時間 細かな見積もりは難しい。 1機能だけに絞り、おおよそで見積 もる。 ○ アプリケーション全体に適用するのは工数が読めない ○ まずは1機能(イベントを作成するフォーム)のみ適用してみるこ とに ■ 理由: アプリ内で状態管理が最も複雑な場所、かつ新規開発で 最も変更が入るため プロダクトの中で生まれた課題と提案 23

Slide 24

Slide 24 text

(4)コード変更の範囲 まずは1機能に絞り、変更の範囲を最小限にする ○ 最終的にはシナリオテストを実施するので、degradeもここで 防げると判断 プロダクトの中で生まれた課題と提案 24

Slide 25

Slide 25 text

(5)チームの学習コスト 既存のコード設計に近いツールを選択する ○ Zustand自体、現在利用しているuseReducerの設計に近い ○ 実は一部すでにZustandを使っていたり、メンバーが利用経験 あったりした ○ Zustand自体がシンプルであり、ドキュメントもしっかりしている ■ -> チームで簡易的なハンズオンを行えば問題ないと判断 プロダクトの中で生まれた課題と提案 25

Slide 26

Slide 26 text

プロダクトの中で生まれた課題と提案 26 導入してみよう!

Slide 27

Slide 27 text

導入までの流れ 1. 新規プロジェクトが動き出すタイミングで「新しいツールを導入したい」旨を チームに伝える a. 現状何に課題を持っているのか、それを導入することでどう変わるか 2. 事前調査を行い、一部コードに適用してみる 3. チーム内で成果を共有し、最終的な同意をとる 4. コード全体に適用 27 プロダクトの中で生まれた課題と提案

Slide 28

Slide 28 text

導入までの流れ(現在) 1. 新規プロジェクトが動き出すタイミングで「新しいツールを導入したい」旨を チームに伝える a. 現状何に課題を持っているのか、それを導入することでどう変わるか 2. 事前調査を行い、一部コードに適用してみる 3. チーム内で成果を共有し、最終的な同意をとる 4. コード全体に適用 28 プロダクトの中で生まれた課題と提案 実はまだ終わってません。。😅 (どこかでお話しできたらいいなぁ。。)

Slide 29

Slide 29 text

29 Zustandを導入してみて (実装の話)

Slide 30

Slide 30 text

実装の話: 前提条件 ● ページを初回開いたとき、DBから保存したデータを読み取り初期化する ● 保存ボタンを押すと、DBへデータを反映する 30 Zustandを導入してみて

Slide 31

Slide 31 text

実装の話: useContextの場合 31 Zustandを導入してみて export default function ContextPage() { ... // 初期データの読み込み useLoadInitialContextData(); return ( {/** body */} ); }
 export const ColorSelector = () => { const { buttonColor, setButtonColor } = useContextStore(); return ( ... setButtonColor(e.target.value)} /> ... ); }; 
 /(root)/page.tsx /(root)/components/ColorSelector.tsx 何が問題か... 


Slide 32

Slide 32 text

実装の話: useContextの場合 32 Zustandを導入してみて export default function ContextPage() { ... // 初期データの読み込み useLoadInitialContextData(); return ( {/** body */} ); }
 export const ColorSelector = () => { const { buttonColor, setButtonColor } = useContextStore(); return ( ... setButtonColor(e.target.value)} /> ... ); }; 
 /(root)/page.tsx /(root)/components/ColorSelector.tsx 何が問題か... 
 buttonColorを変更すると、useContextStoreを参 照している他コンポーネントも再レンダリングが発 生する


Slide 33

Slide 33 text

実装の話: Zustandの場合 33 Zustandを導入してみて export default function ClientZustandPage() { ... // 初期データの読み込み useLoadInitialClientData(); return ( <> {/** body */} ); } export const ColorSelector = () => { const { buttonColor, setButtonColor } = useZustandClientStore( useShallow((state) => ({ buttonColor: state.buttonColor, setButtonColor: state.setButtonColor, })) ); return ( ... setButtonColor(e.target.value)} /> ... ); }; /(root)/page.tsx /(root)/components/ColorSelector.tsx POINT
 ・Providerが不要になる 
 ・各コンポーネントで必要な値のみ参照することで、 他のコンポーネントにも影響が出ない 


Slide 34

Slide 34 text

実装の話: Zustandの場合 34 Zustandを導入してみて 実際に動かしてみると ● 影響範囲のみに再レンダリングが走 ることが確認できる

Slide 35

Slide 35 text

補足: Next.js + Zustand で気をつけたいこと ● (1)storeをクライアント側で管理すること ● (2)storeをまるごと参照しないこと 35 Zustandを導入してみて

Slide 36

Slide 36 text

(1)storeをクライアント側で管理すること ● Next.jsでは、サーバー側でstoreを作成・管理できる ● ただし、サーバーでstoreを持つと任意のユーザと状態が共有されてしまう 可能性がある ○ -> 例) プライバシーの侵害、意図しない値の更新など 参考:https://zustand.docs.pmnd.rs/guides/nextjs 36 Zustandを導入してみて

Slide 37

Slide 37 text

(2)storeをまるごと参照しないこと ● ページコンポーネントでstoreをまるごと参 照する ○ -> いずれかの値が変更された時、ページ全体 が再レンダリングされる ● 回避するには... ○ 参照用に部分的にuseContextを使う ○ 参照用のグローバル変数を別で定義する ○ (他にもあるかも) 37 Zustandを導入してみて export const TodoContainer = () => { const { data, openModal, saveToStorage } = useZustandClientStore( useShallow((state) => ({ data: state, ... })) ); // DBへ保存 const handleSave = useCallback(async () => { await saveToStorage(data); }, [saveToStorage, data]); return ( <> {/** 各入力フォーム */} 一旦保存する ); }; /(root)/page.tsx

Slide 38

Slide 38 text

より深く掘り下げたいあなたへ ● サンプルコード(拙いコードです、ご了承ください ) https://github.com/ooyoshi00/zustand_context_comparison ● 公式: Zustand > Comparison https://zustand.docs.pmnd.rs/getting-started/comparison ● 公式: Jotai > Comparison https://jotai.org/docs/basics/comparison 38 Zustandを導入してみて

Slide 39

Slide 39 text

39 まとめ

Slide 40

Slide 40 text

何を伝えたかったか ● 新しいことを始めるには、準備とタイミングが大事。 ● (規模によるが)導入までには時間がかかる。熱量が大事。 ● Zustandはイイゾ! 最後に これから新しいツールを導入したい! となったときの事例として、活用していただければ幸いです。 40 まとめ

Slide 41

Slide 41 text

41 ご清聴ありがとうございました

Slide 42

Slide 42 text

街中ですれ違う名前も知らない「あの人」も、「あなた」も誰もが無限の可能性を 持っている。そして、「あなた」がどんな場所に暮らしていても、どんなに他人から 無理だと言われても、諦めて欲しくない。 そんな想いから、 Rebaseは生まれました。挑戦の一歩を軽くすることで、人生に 大きな変化を生み出すサポートをしたい。 誰かの人生に彩を与える「ことのはじまり」を、一緒に作りませんか。 Let's work together! https://jobs.rebase.co.jp/