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

プロダクト改善のために新しいことを始める -useContextからの卒業、Zustandへ-

プロダクト改善のために新しいことを始める -useContextからの卒業、Zustandへ-

「useContextじゃもう無理かも」──そんな気づきから始まった、Zustand導入の話。 フロントエンド・バックエンドの垣根なく開発を進めるRebaseの現場で、担当プロダクトの大規模改修を機に、Reactの状態管理や再レンダリングの課題にどう向き合ったのかを振り返ります。

More Decks by 株式会社Rebase_エンジニアリング

Other Decks in Programming

Transcript

  1. 自己紹介 株式会社Rebase エンジニアリング @y-okai 経歴 • 不動産テック企業 エンジニア・スクラムマスター • 株式会社Rebase

    エンジニア 主な仕事 • コミュニティイベントサービス「TOIRO」の開発全般を 担当 • フルスタックには動いていますが、 どちらかというとフロントエンド寄り 4
  2. 事業紹介 ・ イベントの作成 ・ 集客 ・ クレジットカード決済 ・ QRコード受付 ・

    オープンチャット(TOIROG) TOIRO(https://toiro.com/) これまでにない新たなイベント体験を提供するコミュニティイベントサービス イベントを主催する方々にとって便利な機能を順次開発し実装していく予定です
  3. 使っている主な技術 • フロントエンド ◦ TypeScript ◦ React.js ◦ Next.js ◦

    TailwindCSS • バックエンド ◦ TypeScript ◦ Node.js ◦ GCP, Firebase ◦ Terraform TOIROについて 7
  4. 解決策を考える • 案1) リファクタリングしてなんとかする ◦ Contextを分割する ◦ React.memoを利用する ◦ useMemoを利用する

    • 案2) 新しいツールを導入する プロダクトの中で生まれた課題と提案 15 課題1) 
  管理が必要な状態が増える 
 課題2)
  対象箇所全てにメモ化処理が必要(開発者に依存) 

  5. 解決策を考える • 案1) リファクタリングしてなんとかする ◦ Contextを分割する ◦ React.memoを利用する ◦ useMemoを利用する

    • 案2) 新しいツールを導入する(こっちで考える) プロダクトの中で生まれた課題と提案 16
  6. (1)ツールの選定 • 候補 プロダクトの中で生まれた課題と提案 20 ライブラリ 記述量 Provider必須 設計の柔軟性 グローバル状態

    ライブラリサイズ Zustand 少ない 不要 高い 可能 小さい Jotai 少ない 不要 高い 可能 小さい Redux Toolkit 多い 必要 標準的 可能 中〜大 • 考慮したいこと ◦ 機能拡張の増加(== 管理する状態が増える)に耐えられるもの ◦ 学習コストを減らすため、記述量が少なくシンプルなもの
  7. (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 ( <div> <div>{count}</div> <button onClick={() => setCount(c => c + 1)}>+1</button> <input value={text} onChange={e => setText(e.target.value)} /> </div> ) } 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 ( <div> <div>{count}</div> <button onClick={inc}>+1</button> <input value={text} onChange={e => setText(e.target.value)} /> </div> ) } • ZustandはuseReducer、JotaiはuseStateに コード設計が近い • Zustandはセレクタもstoreのなかで管理できる  → Zustandを選択
  8. 実装の話: useContextの場合 31 Zustandを導入してみて export default function ContextPage() { ...

    // 初期データの読み込み useLoadInitialContextData(); return ( <ContextProvider> {/** body */} </ContextProvider> ); }
 export const ColorSelector = () => { const { buttonColor, setButtonColor } = useContextStore(); return ( ... <label className='flex items-center gap-2'> <input type='radio' name='color' value='#3B82F6' checked={buttonColor === '#3B82F6'} onChange={(e) => setButtonColor(e.target.value)} /> <span>青</span> </label> ... ); }; 
 /(root)/page.tsx /(root)/components/ColorSelector.tsx 何が問題か... 

  9. 実装の話: useContextの場合 32 Zustandを導入してみて export default function ContextPage() { ...

    // 初期データの読み込み useLoadInitialContextData(); return ( <ContextProvider> {/** body */} </ContextProvider> ); }
 export const ColorSelector = () => { const { buttonColor, setButtonColor } = useContextStore(); return ( ... <label className='flex items-center gap-2'> <input type='radio' name='color' value='#3B82F6' checked={buttonColor === '#3B82F6'} onChange={(e) => setButtonColor(e.target.value)} /> <span>青</span> </label> ... ); }; 
 /(root)/page.tsx /(root)/components/ColorSelector.tsx 何が問題か... 
 buttonColorを変更すると、useContextStoreを参 照している他コンポーネントも再レンダリングが発 生する

  10. 実装の話: 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 ( ... <label className='flex items-center gap-2'> <input type='radio' name='color' value='#3B82F6' checked={buttonColor === '#3B82F6'} onChange={(e) => setButtonColor(e.target.value)} /> <span>青</span> </label> ... ); }; /(root)/page.tsx /(root)/components/ColorSelector.tsx POINT
 ・Providerが不要になる 
 ・各コンポーネントで必要な値のみ参照することで、 他のコンポーネントにも影響が出ない 

  11. (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 ( <> {/** 各入力フォーム */} <button onClick={handleSave} > 一旦保存する </button> </> ); }; /(root)/page.tsx
  12. より深く掘り下げたいあなたへ • サンプルコード(拙いコードです、ご了承ください ) 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を導入してみて