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

Recoilと将棋ったー

na2hiro
January 20, 2023
2.3k

 Recoilと将棋ったー

将棋対局サイト「将棋ったー」における、Recoilによる将棋盤面の状態管理の事例を紹介します。

イベント:Harajuku.ts Meetup 〜 Recoilの事例集めました〜 https://babel-jp.connpass.com/event/263696/

na2hiro

January 20, 2023
Tweet

Transcript

  1. 自己紹介: @na2hiro • Indeed Japanで口コミを集めるSenior Software Engineer、勤続~8 年 ◦ indeed.jobs?

    • 昨夏東京から札幌に移住し、リモートワーク中 • 最近は Remix にハマっている
  2. フロントエンド状態管理の役割分担 • React Router / Remix ◦ ページ遷移 / URLから派生する状態

    ◦ writeリクエストのハンドリング • Recoil ◦ 将棋盤 • React の useState ◦ その他
  3. 盤の表示: 棋譜atomを定義 { board: [ [“!香”, “!桂”, …, “!桂”, “!香”],

    [“”, “!飛”, …, “馬”, “”], … [“”, “”, …, “飛”, “”], [“香”, “桂”, … “桂”, “香”], ], // mochigoma: (今回は省略), history: [ “77->76”, “33->34”, “88->22 nari” ] } satisfies Shogi
  4. 盤の表示: 棋譜atomを定義 { board: [ [“!香”, “!桂”, …, “!桂”, “!香”],

    [“”, “!飛”, …, “馬”, “”], … [“”, “”, …, “飛”, “”], [“香”, “桂”, … “桂”, “香”], ], // mochigoma: (今回は省略), history: [ “77->76”, “33->34”, “88->22 nari” ] } satisfies Shogi // 棋譜状態 const shogiState = atom<Shogi>({ key: "shogiState", default: null, }) ※ 省スペースのためのおことわり ・atom, selectorのkeyは省略します。  (変数名と同じ) ・constの代わりにlet
  5. 盤の表示: 棋譜atomを定義 { board: [ [“!香”, “!桂”, …, “!桂”, “!香”],

    [“”, “!飛”, …, “馬”, “”], … [“”, “”, …, “飛”, “”], [“香”, “桂”, … “桂”, “香”], ], // mochigoma: (今回は省略), history: [ “77->76”, “33->34”, “88->22 nari” ] } satisfies Shogi // 棋譜状態 const shogiState = atom<Shogi>({ key: "shogiState", default: null, }) shogi ※ 省スペースのためのおことわり ・atom, selectorのkeyは省略します。  (変数名と同じ) ・constの代わりにlet
  6. 盤の表示: 棋譜atomを初期化 { board: [ [“!香”, “!桂”, …, “!桂”, “!香”],

    [“”, “!飛”, …, “馬”, “”], … [“”, “”, …, “飛”, “”], [“香”, “桂”, … “桂”, “香”], ], // mochigoma: (今回は省略), history: [ “77->76”, “33->34”, “88->22 nari” ] } satisfies Shogi // 棋譜状態 let shogiState = atom<Shogi>({ default: null, }) // 盤コンポーネント let Game = ({shogi}) => ( <RecoilRoot initializeState={({set})=>{ set(shogiState, shogi) }}> <Board /> </RecoilRoot> )
  7. 盤の表示: 棋譜データを使う { board: [ [“!香”, “!桂”, …, “!桂”, “!香”],

    [“”, “!飛”, …, “馬”, “”], … [“”, “”, …, “飛”, “”], [“香”, “桂”, … “桂”, “香”], ], // mochigoma: (今回は省略), history: [ “77->76”, “33->34”, “88->22 nari” ] } satisfies Shogi // 駒コンポーネント let Koma = ({x, y}: Pos) => { let {board} = useRecoilValue(shogiState) return <button>{board[x][y]}</button> } // 棋譜状態 let shogiState = atom<Shogi>({ default: null, })
  8. 駒の可動域の表示: 掴んだ駒の座標atomを定義 { board: [ [“!香”, “!桂”, …, “!桂”, “!香”],

    [“”, “!飛”, …, “馬”, “”], … [“”, “”, …, “飛”, “”], [“香”, “桂”, … “桂”, “香”], ], // mochigoma: (今回は省略), history: [ “77->76”, “33->34”, “88->22 nari” ] } satisfies Shogi // 棋譜状態 let shogiState = atom<Shogi>({...}) // 掴んだ座標 let selectedPosState = atom<Pos>({ default: null, })
  9. 駒の可動域の表示: 掴んだ駒の可動域selectorを定義 { board: [ [“!香”, “!桂”, …, “!桂”, “!香”],

    [“”, “!飛”, …, “馬”, “”], … [“”, “”, …, “飛”, “”], [“香”, “桂”, … “桂”, “香”], ], // mochigoma: (今回は省略), history: [ “77->76”, “33->34”, “88->22 nari” ] } satisfies Shogi // 棋譜状態 let shogiState = atom<Shogi>({...}) // 掴んだ座標 let selectedPosState = atom<Pos>({...}) // 掴んだ駒の可動域リスト let movableState = selector<Pos[]>({ get: ({ get }) => { let {board} = get(shogiState) let pos = get(selectedPosState) return calculateMovable(board, pos) } })
  10. 駒の可動域の表示: 掴んだ駒の可動域selectorを定義 { board: [ [“!香”, “!桂”, …, “!桂”, “!香”],

    [“”, “!飛”, …, “馬”, “”], … [“”, “”, …, “飛”, “”], [“香”, “桂”, … “桂”, “香”], ], // mochigoma: (今回は省略), history: [ “77->76”, “33->34”, “88->22 nari” ] } satisfies Shogi // 棋譜状態 let shogiState = atom<Shogi>({...}) // 掴んだ座標 let selectedPosState = atom<Pos>({...}) // 掴んだ駒の可動域リスト let movableState = selector<Pos[]>({ get: ({ get }) => { let {board} = get(shogiState) let pos = get(selectedPosState) return calculateMovable(board, pos) } }) shogi select edPos mov able
  11. 駒の可動域の表示: 掴んだ駒の可動域selectorの読込み { board: [ [“!香”, “!桂”, …, “!桂”, “!香”],

    [“”, “!飛”, …, “馬”, “”], … [“”, “”, …, “飛”, “”], [“香”, “桂”, … “桂”, “香”], ], // mochigoma: (今回は省略), history: [ “77->76”, “33->34”, “88->22 nari” ] } satisfies Shogi // 棋譜状態 let shogiState = atom<Shogi>({...}) // 掴んだ座標 let selectedPosState = atom<Pos>({...}) // 掴んだ駒の可動域リスト let movableState = selector<Pos[]>({ key: "movableState", get: ({ get }) => { let {board} = get(shogiState) let pos = get(selectedPosState) return calculateMovable(board, pos) } }) // 駒コンポーネント let Koma = ({x, y}: Pos) => { let {board} = useRecoilValue(shogiState) let movables = useRecoilValue(movableState) return <button style={isIn(movables, x, y)}> {board[x][y]} </button> }
  12. 駒の可動域の表示: 掴んだ駒の可動域selectorの書込み { board: [ [“!香”, “!桂”, …, “!桂”, “!香”],

    [“”, “!飛”, …, “馬”, “”], … [“”, “”, …, “飛”, “”], [“香”, “桂”, … “桂”, “香”], ], // mochigoma: (今回は省略), history: [ “77->76”, “33->34”, “88->22 nari” ] } satisfies Shogi // 棋譜状態 let shogiState = atom<Shogi>({...}) // 掴んだ座標 let selectedPosState = atom<Pos>({...}) // 掴んだ駒の可動域リスト let movableState = selector<Pos[]>({ key: "movableState", get: ({ get }) => { let {board} = get(shogiState) let pos = get(selectedPosState) return calculateMovable(board, pos) } }) // 駒コンポーネント let Koma = ({x, y}: Pos) => { let {board} = useRecoilValue(shogiState) let movables = useRecoilValue(movableState) let setSelectedPos = useSetRecoilValue(selectedPosState) return <button style={isIn(movables, x, y)} onClick={()=>setSelectedPos({x, y})}> {board[x][y]} </button> }
  13. 駒の可動域の表示: 掴んだ駒を離すには? (愚直) // 駒コンポーネント let Koma = ({x, y}:

    Pos) => { let {board} = useRecoilValue(shogiState) let movables = useRecoilValue(movableState) let [selectedPos, setSelectedPos] = useSetRecoilState(selectedPosState) return <button style={isIn(movables, x, y)} onClick={()=>{ if(eq(selectedPos, {x, y}) { setSelectedPos(null) } else { setSelectedPos({x, y}) } }> {board[x][y]} </button> } コンポーネントで ゴチャゴチャや るのか? selectedPosを 監視しないとい けなくなった ❓ // 掴んだ座標 let selectedPosState = atom<Pos>({ default: null, }) 他で使うときに 実装し忘れそう
  14. 駒の可動域の表示: 掴んだ駒を離すには? // 掴んだ座標 let _selectedPosState = atom<Pos>({ default: null,

    }) let selectedPosState = selector<Pos>({ get: ({get}) => get(_selectedPosState), set: ({get, set}, newPos) => { if(eq(get(_selectedPosState), newPos))) { set(null) } else { set(newPos) } } } // 掴んだ座標 let selectedPosState = atom<Pos>({ default: null, }) Recoil パターン 1: カプセル化 atomをselectorでラップすることで、状態 の更新に関するロジックをカプセル化で きる。atomをexportしなければそれを必 須化できる。 // 駒コンポーネント (当初から変更なし) let Koma = ({x, y}: Pos) => { let {board} = useRValue(shogiState) let movables = useRValue(movableState) let setSelectedPos = useSetRecoilValue(selectedPosState) return <button style={isIn(movables, x, y)} onClick={()=>setSelectedPos({x,y})}> {board[x][y]} </button> }
  15. 駒の可動域の表示: 掴んだ駒を離すには? // 掴んだ座標 let _selectedPosState = atom<Pos>({ default: null,

    }) let selectedPosState = selector<Pos>({ get: ({get}) => get(_selectedPosState), set: ({get, set}, newPos) => { if(eq(get(_selectedPosState), newPos))) { set(null) } else { set(newPos) } } } Recoil パターン 1: カプセル化 atomをselectorでラップすることで、状態 の更新に関するロジックをカプセル化で きる。atomをexportしなければそれを必 須化できる。 shogi select edPos mova ble _select edPos
  16. 棋譜の再生: 手数atom、再生後の盤面selectorを定義 // 棋譜状態 let shogiState = atom<Shogi>({...}) // 手数

    let tesuuState = atom<number>({default: 0}) // boardがtesuu手目の状態になった棋譜状態 let replayedShogiState = selector<Shogi>({ get: ({get}) => { let shogi = get(shogiState) let tesuu = get(tesuuState) return calcReplayed(shogi, tesuu) } }) shogi select edPos mova ble _select edPos repla yedS hogi tesuu
  17. 棋譜の再生: 再生後の盤面selectorを使うのはどこ? // 棋譜状態 let shogiState = atom<Shogi>({...}) // 手数

    let tesuuState = atom<number>({default: 0}) // boardがtesuu手目の状態になった棋譜状態 let replayedShogiState = selector<Shogi>({ get: ({get}) => { let shogi = get(shogiState) let tesuu = get(tesuuState) return calcReplayed(shogi, tesuu) } }) • 盤面を表示するとき • 駒を掴むとき • 掴んだ駒の可動域を計算す るとき →ほぼすべて。 // 駒コンポーネント let Koma = ({x, y}: Pos) => { let {board} = useRecoilValue(replayedShogiState) return <button>{board[x][y]}</button> } ❓ 使ってる箇所を全 部変えないといけ ない?
  18. 棋譜の再生: shogiStateを再生後の盤面selectorで置換 // 最新の盤面を持った棋譜状態 let latestShogiState = atom<Shogi>({...}) // 手数

    let tesuuState = atom<number>({default: 0}) // boardがtesuu手目の状態になった棋譜状態 let shogiState = selector<Shogi>({ get: ({get}) => { let shogi = get(latestShogiState) let tesuu = get(tesuuState) return calcReplayed(shogi, tesuu) } }) • 盤面を表示するとき • 駒を掴むとき • 掴んだ駒の可動域を計算す るとき →ほぼすべて。 再生後の盤面selectorを shogiStateと命名すれば、コン ポーネント側の変更は不要 Recoil のいいところ 1: 状態のリファク タリングがしやすい atom・selectorの区別なく、変数とkeyの変 名だけでリファクタリングできる
  19. 棋譜の再生: shogiStateを再生後の盤面selectorで置換 // 最新の盤面を持った棋譜状態 let latestShogiState = atom<Shogi>({...}) // 手数

    let tesuuState = atom<number>({default: 0}) // boardがtesuu手目の状態になった棋譜状態 let shogiState = selector<Shogi>({ get: ({get}) => { let shogi = get(latestShogiState) let tesuu = get(tesuuState) return calcReplayed(shogi, tesuu) } }) shogi select edPos mova ble _select edPos tesuu latest Shogi
  20. 棋譜の再生: shogiStateを再生後の盤面selectorで置換 shogi select edPos mova ble _select edPos tesuu

    latest Shogi shogi select edPos mova ble _select edPos repla yedS hogi tesuu
  21. 棋譜の再生: 手数atomへの書込み // 最新の盤面を持った棋譜状態 let latestShogiState = atom<Shogi>({...}) // 手数

    let _tesuuState = atom<number>({default: 0}) let tesuuState = selector<number>({ get: ({get})=> get(_tesuuState), set: ({set, get}, newTesuu) => { let {history} = get(latestShogiState) if(0>newTesuu || history.length-1 > newTesuu) return // setしない set(_tesuuState, newTesuu) } }) Recoil パターン 2: validation カプセル化の後、set時に値を検査しはじ く
  22. 棋譜の再生: 最新盤面への追従機能 // 最新の盤面を持った棋譜状態 let latestShogiState = atom<Shogi>({...}) // 手数

    let _tesuuState = atom<number>({default: Infinity}) let tesuuState = selector<number>({ get: ({get})=> Math.min( get(_tesuuState), get(latestShogiState).history.length-1)) set: ({set, get}, newTesuu) => { let {history} = get(latestShogiState) if(0>newTesuu) return // setしない set(_tesuuState, history.length-1 <= newTesuu ? Infinity : newTesuu) } }) 最新の盤面が表示されてい るときに新しい盤面が配信 されたら、最新盤面に再生 させたい Recoil パターン 3: normalization カプセル化の後、get時に値を検査し調整 する
  23. Recoil の良いところ2 - APIが簡潔 • Reactの状態hooksと似たようなメンタルモデルで書ける ◦ useState, useCallback ◦

    useRecoilState, useRecoilCallback • 大きなフォントを使ったスライドに収まるくらいボイラープレートが少ない • atom, selectorのような変数的なものは変数で定義するので、直感的(ス コープはグローバルではなくRecoilRootごとではあるが)
  24. Recoil の良いところ3 - ボトムアップ(モデル先行) • Recoil ◦ ボトムアップで概念のモデルを構築し、使うところで hookする ◦

    IMHO: モデルがさき、使う側があと ◦ 局所性も持たせられる。 e.g. 複数の将棋盤を同時に表示 ▪ <RecoilRoot>ごとに違う状態を持たせられる • Redux ◦ トップダウンでアプリで使いたい概念を全部入れ、使うものを reducerで取り出す ◦ IMHO: アプリがさき、モデルがあと ◦ 基本は1つのストアに使いたいものを全部入れる ▪ FAQ: 複数のストアは使えるけどパフォーマンスなどの観点でおすすめしないよ Fetch系ライブラリやReact Router/Remixなど、特定の状態を巧妙に管理し てくれるライブラリは多い。ボトムアップのほうが状態管理の適材適所に向いて いるのでは?
  25. Recoil のパターンと良いところ recap Recoil パターン 1: カプセル化 atomをselectorでラップすることで、状態 の更新に関するロジックをカプセル化で きる。atomをexportしなければそれを必

    須化できる。 Recoil のいいところ 1: 状態のリファ クタリングがしやすい atom・selectorの区別なく、変数とkeyの変 名だけでリファクタリングできる Recoil パターン 3: normalization カプセル化の後、get時に値を検査し調整 する Recoil パターン 2: validation カプセル化の後、set時に値を検査しはじ く Recoil のいいところ 2: APIが簡潔 Recoil のいいところ 3: ボトムアップ(モ デル先行)
  26. Q: useRecoilCallbackを使う時は? A: useCallbackを使う時 && callback内でRecoil状態を読み書きする時 useCallbackを使う時とは? callback内でRecoil状態を読み書きする時 • selectorのset内でgetすることもできるの

    で、callback内でRecoil状態を読みつつ 書き込む必要はないかも • selectorの定義にするほど共通の処理で はない場合に使えるかも (React docより)
  27. // 駒コンポーネント let Koma = ({x, y}: Pos) => {

    ... let setSelectedPos = useSetRecoilValue(selectedPosState) return <button ... onClick={()=>setSelectedPos({x, y})}> {board[x][y]} </button> } 駒を掴む場合に使いたいか? ❎useCallbackを使ってもしょうが ない ❎callback内でRecoil状態を読む わけではない useCallbackを使う時 (React docより) Q: useRecoilCallbackを使う時は? A: useCallbackを使う時 && callback内でRecoil状態を読み書きする時