私は今までMobXを使った事はありませんでした。またReduxに比べ利用者は少ないようなので React用ステート管理2020 〜Recoilを試してみました〜でMobXは無視しましたが、気にはなっていたので試してみました。
MobX 6.0リリースおめでとうございます!
MobXのサンプルの解説
MobXはシンプルでスケーラブルなステート管理ライブラリーです、オブザーバー・パターン(Observer)を使ってステートの変化をViewに伝えいます。
今回はmobx-react-liteを使ってみました。従来のMobXは、クラスコンポーネントに@observer
等のデコレータを使いMobXの設定を行うスタイルでしたが、mobx-react-liteはデコレータを使わず、Hooksを使う関数コンポーネント専用に作られたライブラリーです。
MobXのステートを共有する方法は MobX React integrationにあるように、いくつかの方法がありますが、今回のサンプルではReactのContextを使う事にしました。またMobx-React-Lite for Hooks in a nutshell with code sampleを参考にしました。
- JyankenContext.tsxがMobX(mobx-react-lite)の核になるモジュールです
- MobXのObservableオブジェクト(ステートやアクション)が格納さるJyankenContextの作成
- JyankenProviderはJyankenContextのProviderの定義で、内部にuseLocalObservable APIで、Observableオブジェクトを定義しています
- JyankenBox.tsxではContextでObservableオブジェクトを取得し、アクションを呼び出しています
- ScoreBox.tsxはステートの変更で再表示するようにobserver APIでくくっています。またContextでObservableオブジェクトを取得しています
まとめ
Mobxは元祖Reduxに比べるとコード量が少なく使いやすいライブラリーと言われてきましたが、 Redux ToolkitやReactのuseReducerとContextがリリースされた現在ではコード量は同程度であり、コードの構造も似たものになっています。
React用ステート管理2020 〜Recoilを試してみました〜を更新しMobXを追加しました。
サンプルコード
MobX
- JyankenContext.tsx
import React, { createContext } from 'react'
import { useLocalObservable } from 'mobx-react-lite'
export enum Jjudgment { Draw = 0, Win, Lose }
export enum Te { Guu = 0, Choki, Paa}
export type ScoreType = {
human: number,
computer: number,
judgment: Jjudgment
}
export type JyankenType = {
scores: ScoreType[]
pon: (human: Te) => void
}
export const JyankenContext = createContext({} as JyankenType)
export const JyankenProvider: React.FC = ({ children }) => {
const store = useLocalObservable<JyankenType>(() => ({
scores: [],
pon(human: Te) {
const computer:Te = Math.floor(Math.random() * 3)
const judgment:Jjudgment = (computer - human + 3) % 3
const score = {human: human, computer: computer, judgment: judgment}
store.scores = [...store.scores, score]
}
}))
return <JyankenContext.Provider value={store}>{children}</JyankenContext.Provider>
}
- Jyanken.tsx
import React from 'react'
import JyankenBox from './JyankenBox'
import ScoreBox from './ScoreBox'
import { JyankenProvider } from './JyankenContext'
const Jyanken = () => {
return (
<JyankenProvider>
<h1>じゃんけん ポン!</h1>
<JyankenBox />
<ScoreBox />
</JyankenProvider>
)
}
export default Jyanken
- JyankenBox.tsx
import React, { useContext } from 'react'
import { JyankenContext, Te } from './JyankenContext'
const JyankenBox: React.FC = () => {
const divStyle: React.CSSProperties = {margin: "0 20px"}
const buttonStyle: React.CSSProperties = {margin: "0 10px", padding: "3px 10px", fontSize: 14}
const store = useContext(JyankenContext)
return (
<div style={divStyle}>
<button onClick={() => store.pon(Te.Guu)} style={buttonStyle}>グー</button>
<button onClick={() => store.pon(Te.Choki)} style={buttonStyle}>チョキ</button>
<button onClick={() => store.pon(Te.Paa)} style={buttonStyle}>パー</button>
</div>
)
}
export default JyankenBox
- ScoreBox.tsx
import React, { useContext } from 'react'
import { observer } from 'mobx-react-lite'
import { JyankenContext } from './JyankenContext'
const ScoreBox: React.FC = observer(() => {
const teString = ["グー","チョキ", "パー"]
const judgmentString = ["引き分け","勝ち", "負け"]
const tableStyle: React.CSSProperties = {marginTop: 20, borderCollapse: "collapse"}
const thStyle: React.CSSProperties = {border: "solid 1px #888", padding: "3px 15px"}
const tdStyle: React.CSSProperties = {border: "solid 1px #888", padding: "3px 15px", textAlign : "center"}
const scores = useContext(JyankenContext).scores
return (
<table style={tableStyle}>
<thead>
<tr>
<th style={thStyle}>あなた</th>
<th style={thStyle}>コンピュター</th>
<th style={thStyle}>勝敗</th>
</tr>
</thead>
<tbody>
{scores.map((score, ix) =>
<tr key={ix}>
<td style={tdStyle}>{teString[score.human]}</td>
<td style={tdStyle}>{teString[score.computer]}</td>
<td style={tdStyle}>{judgmentString[score.judgment]}</td>
</tr>
)}
</tbody>
</table>
)
})
export default ScoreBox
- index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import Jyanken from './Jyanken'
ReactDOM.render(
<React.StrictMode>
<Jyanken />
</React.StrictMode>,
document.getElementById('root')
)