ReduxからJotaiへのマイグレーション体験(超入門)
はじめに
Reactの状態管理ライブラリであるReduxのコードを、Jotaiに書き換えて紹介している記事はなさそうだったので、簡単なカウンターアプリを題材にします。
Redux公式ドキュメントで紹介されているサンプルコード付きのものがあったのでそれを対象にします。
Redux Essentials, Part 2: Redux App Structure の The Counter Example App
今回はこちらをJotaiで書き換えたいと思います。
まずはReduxのコードを確認し、それに対応するJotaiでの書き方を確認します。
JotaiはReduxのようにボイラープレートな書き方にはならず、自由です。体験、超入門とタイトル付けしましたが、書き方は一例として見てください。細かなReduxやJotaiの説明は省きます。記事の最後にJotaiに関する記事のURLを載せてますので、参考にして頂ければです。
対象読者
- Jotai未経験のすべてのReactユーザー
- 現役Reduxユーザー〜最近使っていない人まで
- Recoil, Jotaiに興味のある人
ReduxのThe Counter Example Appを確認
Storeの定義と設定
Provider設定
Providerへstoreを設定していますね。
...(省略)...
import { Provider } from 'react-redux';
import store from './app/store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
...(省略)...
Storeの定義
先程のstoreの中身です。今回はcounterですね。Redux Tool Kitが使われています。
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export default configureStore({
reducer: {
counter: counterReducer,
},
});
counter state と update関数 の定義
storeへ定義したcounterの中身です。コード内にコメントとしていくつかの説明をします。
import { createSlice } from '@reduxjs/toolkit'
// counterのstateと3つのreducer。Redux Tool KitではImmerが採用されています。
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (state, action) => { // ユーザーが入力した数字で加算
state.value += action.payload
},
},
})
// 各reducerをexport
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export const incrementAsync = (amount) => (dispatch) => {
setTimeout(() => {
dispatch(incrementByAmount(amount))
}, 1000)
}
// セレクタ関数。counterのstateであるcountを指定
export const selectCount = (state) => state.counter.value
export default counterSlice.reducer
Counterコンポーネント
見やすさのためにcss部分とaria-labelの記述を削っています。
特に説明は不要でしょうか。
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
decrement,
increment,
incrementByAmount,
incrementAsync,
selectCount,
} from './counterSlice';
export function Counter() {
const count = useSelector(selectCount);
const dispatch = useDispatch();
const [incrementAmount, setIncrementAmount] = useState('2');
return (
<div>
<div>
<button onClick={() => dispatch(increment())}>
+
</button>
<span>{count}</span>
<button onClick={() => dispatch(decrement())}>
-
</button>
</div>
<div>
<input
value={incrementAmount}
onChange={e => setIncrementAmount(e.target.value)}
/>
<button
onClick={() =>
dispatch(incrementByAmount(Number(incrementAmount) || 0))
}
>
Add Amount
</button>
<button
onClick={() => dispatch(incrementAsync(Number(incrementAmount) || 0))}
>
Add Async
</button>
</div>
</div>
);
}
以上で主要なコードは終了です。以下が実際に動作するプレビューです。
Jotaiへの書き換え
まずは書き換え結果から見てみます。問題なさそうです。
Storeの定義と設定
Provider設定
JotaiはProviderが無くても動作します。今回は不要なので削ります。
...(省略)...
ReactDOM.render(
<App />,
document.getElementById('root')
);
...(省略)...
Storeの定義
JotaiはStoreを一つ一つのatomとして持ちます。なのでReduxのような巨大な1つのStoreにまとめる必要はありません。
counter state と update関数 の定義
counterSlice.jsというファイル名は不適切かもしれませんが、書き換えということでファイル名はこのままに。
今回は、counterなのでカウントの値を1つ持つatomを定義します。
各reducerをwrite-only atomを用いて表現しました。get(countAtom)
でその時のcountAtomの中身を取得して加減算してsetしています。
write関数は第3引数に任意の値を受ける事ができます。例えば、incrementByAmountAtomではamountを受けるようにしてそれで加算しています。使う場面はCounterコンポーネントを見てください。
セレクタ関数は不要です。
お気づきのように、全てatomで表現できます。
import { atom } from "jotai";
export const countAtom = atom(0); // storeであり、counterSliceのinitialState.value部分と同義
export const incrementAtom = atom(null, (get, set) =>
set(countAtom, get(countAtom) + 1)
);
export const decrementAtom = atom(null, (get, set) =>
set(countAtom, get(countAtom) - 1)
);
export const incrementByAmountAtom = atom(null, (get, set, amount) =>
set(countAtom, get(countAtom) + amount)
);
export const incrementAsyncAtom = atom(null, (get, set, amount) => {
setTimeout(() => {
set(countAtom, get(countAtom) + amount);
}, 1000);
});
Counterコンポーネント
定義したatomをJotai APIのhooksを用いてコンポーネントで使えるようにします。
v1.6.0から、値のみを得るuseAtomValue、update関数のみを得るuseSetAtom(旧useUpdateAtom)が提供されたのでそれらを使いました。
const dispatch = useDispatch();
のようなものは不要です。
完全な一致はしませんが、書き換え後の対応関係をいくつか紹介すると、以下のような感じになります。
const count = useSelector(selectCount);
↓
const count = useAtomValue(countAtom);
const dispatch = useDispatch();
dispatch(increment())
↓
const increment = useSetAtom(incrementAtom);
increment()
Counterコンポーネントの書き換え後は以下です。
import React, { useState } from "react";
import { useAtomValue, useSetAtom } from "jotai";
import {
countAtom,
decrementAtom,
incrementAtom,
incrementByAmountAtom,
incrementAsyncAtom
} from "./counterSlice";
export function Counter() {
const count = useAtomValue(countAtom);
const decrement = useSetAtom(decrementAtom);
const increment = useSetAtom(incrementAtom);
const incrementByAmount = useSetAtom(incrementByAmountAtom);
const incrementAsync = useSetAtom(incrementAsyncAtom);
const [incrementAmount, setIncrementAmount] = useState("2");
return (
<div>
<div>
<button onClick={() => increment()}>
+
</button>
<span>{count}</span>
<button onClick={() => decrement()}>
-
</button>
</div>
<div>
<input
value={incrementAmount}
onChange={(e) => setIncrementAmount(e.target.value)}
/>
<button
{/* atom(null, (get, set, amount) => ...) のamountがNumber(incrementAmount) || 0 */}
onClick={() => incrementByAmount(Number(incrementAmount) || 0)}
>
Add Amount
</button>
<button
onClick={() => incrementAsync(Number(incrementAmount) || 0)}
>
Add Async
</button>
</div>
</div>
);
}
おわりに
いかがでしたでしょうか。冒頭にも述べましたが、Jotaiは自由度の高い使い方ができるためReduxのように型にはまった記述は無くなります。Storeもatomベースで、アプリ全体のsingle sourceを気にせず必要な時に定義し使うことが出来ます。
JotaiはReact.useState()
のインターフェースと同じ様に使えるuseAtom()
を提供しています。なので、const [count, setCount] = useAtom(countAtom)
とも書けます。
今回のcounterを例にすると、incrementはsetCount((state) => state + 1)
、decrementはsetCount((state) => state - 1)
、incrementByAmountはsetCount((state) => state + Number(incrementAmount) || 0)
と書くことも出来ます。
今回、Reduxのaction(reducer)部分は、Jotaiのwrite-only atomで表現してみました。プロジェクト内でstateの更新処理を明確に分離しておきたい時にはこの書き方をルールとしても良いかもしれません。
Redux Essentialsにはさらに、social media feed appと題してカウンターアプリより実践的な題材を扱っています。
今後はこれを対象にJotaiでの書き換えを試してみようと思います。
アペンディクス
最新の記事はこちら。
おまけ
Twitterアカウント、フォロー頂けると嬉しいです。
Jotai Friends のテックブログです。 React状態管理ライブラリJotai(jotai.org)を新たな選択肢に。 エンジニアの皆さんが安心してJotaiを採用できるように支援します。 Powered by twitter.com/t6adev
Discussion