Slide 1

Slide 1 text

Recoilと将棋ったー @na2hiro - 2023/1/20

Slide 2

Slide 2 text

自己紹介: @na2hiro ● Indeed Japanで口コミを集めるSenior Software Engineer、勤続~8 年 ○ indeed.jobs? ● 昨夏東京から札幌に移住し、リモートワーク中 ● 最近は Remix にハマっている

Slide 3

Slide 3 text

目次 ● 将棋ったーとは ● フロントエンド状態管理の役割分担 ● 将棋盤を実装していこう ● Recoilのパターンと良いところ

Slide 4

Slide 4 text

将棋ったーとは ● 100種類以上の変則将棋のオンライン対局ができるサイト ○ e.g. 量子将棋、大局将棋、将棋対囲碁、・・・ ● Tech stack ○ Remix (Web framework) ○ React ○ Recoil ● https://shogitter.com

Slide 5

Slide 5 text

フロントエンド状態管理の役割分担 ● React Router / Remix ○ ページ遷移 / URLから派生する状態 ○ writeリクエストのハンドリング ● Recoil ○ 将棋盤 ● React の useState ○ その他

Slide 6

Slide 6 text

将棋盤を 実装していこう

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

盤の表示: 棋譜データを使う { board: [ [“!香”, “!桂”, …, “!桂”, “!香”], [“”, “!飛”, …, “馬”, “”], … [“”, “”, …, “飛”, “”], [“香”, “桂”, … “桂”, “香”], ], // mochigoma: (今回は省略), history: [ “77->76”, “33->34”, “88->22 nari” ] } satisfies Shogi // 駒コンポーネント let Koma = ({x, y}: Pos) => { let {board} = useRecoilValue(shogiState) return {board[x][y]} } // 棋譜状態 let shogiState = atom({ default: null, })

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

駒の可動域の表示: 掴んだ駒の可動域selectorの読込み { board: [ [“!香”, “!桂”, …, “!桂”, “!香”], [“”, “!飛”, …, “馬”, “”], … [“”, “”, …, “飛”, “”], [“香”, “桂”, … “桂”, “香”], ], // mochigoma: (今回は省略), history: [ “77->76”, “33->34”, “88->22 nari” ] } satisfies Shogi // 棋譜状態 let shogiState = atom({...}) // 掴んだ座標 let selectedPosState = atom({...}) // 掴んだ駒の可動域リスト let movableState = selector({ 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 {board[x][y]} }

Slide 16

Slide 16 text

駒の可動域の表示: 掴んだ駒の可動域selectorの書込み { board: [ [“!香”, “!桂”, …, “!桂”, “!香”], [“”, “!飛”, …, “馬”, “”], … [“”, “”, …, “飛”, “”], [“香”, “桂”, … “桂”, “香”], ], // mochigoma: (今回は省略), history: [ “77->76”, “33->34”, “88->22 nari” ] } satisfies Shogi // 棋譜状態 let shogiState = atom({...}) // 掴んだ座標 let selectedPosState = atom({...}) // 掴んだ駒の可動域リスト let movableState = selector({ 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 setSelectedPos({x, y})}> {board[x][y]} }

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

駒の可動域の表示: 掴んだ駒を離すには? // 掴んだ座標 let _selectedPosState = atom({ default: null, }) let selectedPosState = selector({ get: ({get}) => get(_selectedPosState), set: ({get, set}, newPos) => { if(eq(get(_selectedPosState), newPos))) { set(null) } else { set(newPos) } } } // 掴んだ座標 let selectedPosState = atom({ 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 setSelectedPos({x,y})}> {board[x][y]} }

Slide 19

Slide 19 text

駒の可動域の表示: 掴んだ駒を離すには? // 掴んだ座標 let _selectedPosState = atom({ default: null, }) let selectedPosState = selector({ 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

Slide 20

Slide 20 text

棋譜の再生: 手数atom、再生後の盤面selectorを定義 // 棋譜状態 let shogiState = atom({...}) // 手数 let tesuuState = atom({default: 0}) // boardがtesuu手目の状態になった棋譜状態 let replayedShogiState = selector({ 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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

棋譜の再生: shogiStateを再生後の盤面selectorで置換 shogi select edPos mova ble _select edPos tesuu latest Shogi shogi select edPos mova ble _select edPos repla yedS hogi tesuu

Slide 25

Slide 25 text

棋譜の再生: 手数atomへの書込み // 最新の盤面を持った棋譜状態 let latestShogiState = atom({...}) // 手数 let _tesuuState = atom({default: 0}) let tesuuState = selector({ 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時に値を検査しはじ く

Slide 26

Slide 26 text

棋譜の再生: 最新盤面への追従機能 // 最新の盤面を持った棋譜状態 let latestShogiState = atom({...}) // 手数 let _tesuuState = atom({default: Infinity}) let tesuuState = selector({ 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時に値を検査し調整 する

Slide 27

Slide 27 text

Recoil の良いところ2 - APIが簡潔 ● Reactの状態hooksと似たようなメンタルモデルで書ける ○ useState, useCallback ○ useRecoilState, useRecoilCallback ● 大きなフォントを使ったスライドに収まるくらいボイラープレートが少ない ● atom, selectorのような変数的なものは変数で定義するので、直感的(ス コープはグローバルではなくRecoilRootごとではあるが)

Slide 28

Slide 28 text

Recoil の良いところ3 - ボトムアップ(モデル先行) ● Recoil ○ ボトムアップで概念のモデルを構築し、使うところで hookする ○ IMHO: モデルがさき、使う側があと ○ 局所性も持たせられる。 e.g. 複数の将棋盤を同時に表示 ■ ごとに違う状態を持たせられる ● Redux ○ トップダウンでアプリで使いたい概念を全部入れ、使うものを reducerで取り出す ○ IMHO: アプリがさき、モデルがあと ○ 基本は1つのストアに使いたいものを全部入れる ■ FAQ: 複数のストアは使えるけどパフォーマンスなどの観点でおすすめしないよ Fetch系ライブラリやReact Router/Remixなど、特定の状態を巧妙に管理し てくれるライブラリは多い。ボトムアップのほうが状態管理の適材適所に向いて いるのでは?

Slide 29

Slide 29 text

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: ボトムアップ(モ デル先行)

Slide 30

Slide 30 text

おまけ

Slide 31

Slide 31 text

Q: useRecoilCallbackを使う時は? A: useCallbackを使う時 && callback内でRecoil状態を読み書きする時 useCallbackを使う時とは? callback内でRecoil状態を読み書きする時 ● selectorのset内でgetすることもできるの で、callback内でRecoil状態を読みつつ 書き込む必要はないかも ● selectorの定義にするほど共通の処理で はない場合に使えるかも (React docより)

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

Q: useRecoilCallbackを使う時は? A: useCallbackを使う時 && callback内でRecoil状態を読む時 将棋ったーの実際のコミット