はじめに
Redux はそんなに複雑でもないので、公式のドキュメントを見ると、ざっくり理解はできる。
- 公式doc: https://redux.js.org/
手を動かして動作確認をするには、環境構築不要・1HTMLファイルで動作確認できる。保存してブラウザで開けば動作をする。
四つのボタン(+、-、奇数なら+、1秒後に+)があって、押すとカウンタの値が変化する。これを少しいじって、Reduxにより親しんでみよう。
Step#01: 改良点を見つける
これ以上ないほどシンプルなので、コードは読みやすい。しかし改良点がある。つまり、ボタンをクリックしたときに行われる処理がべた書きされているのだ。該当する箇所を示そう。
document.getElementById('increment')
.addEventListener('click', function () {
store.dispatch({ type: 'INCREMENT' })
})
document.getElementById('decrement')
.addEventListener('click', function () {
store.dispatch({ type: 'DECREMENT' })
})
document.getElementById('incrementIfOdd')
.addEventListener('click', function () {
if (store.getState() % 2 !== 0) {
store.dispatch({ type: 'INCREMENT' })
}
})
document.getElementById('incrementAsync')
.addEventListener('click', function () {
setTimeout(function () {
store.dispatch({ type: 'INCREMENT' })
}, 1000)
})
Redux では、Actionを作成するのはActionCreatorの役割だ。カウンターの例ではどのようなActionが作成されるかは自明だが、普通はロジックをこねくり回してどんなアクションが生成されるかが決まる。そこでAction Creatorを使って書き直す必要がありそうだ。上記のコードは、以下のように変更した方がより React+Redux っぽいだろう。
document.getElementById('increment')
.addEventListener('click', function () {
props.increment()
})
document.getElementById('decrement')
.addEventListener('click', function () {
props.decrement()
})
document.getElementById('incrementIfOdd')
.addEventListener('click', function () {
props.incrementIfOdd(store.getState())
})
document.getElementById('incrementAsync')
.addEventListener('click', function () {
props.incrementAsync()
})
ロジックがすべて外だしされてすっきりした。
Step#02: ActionCreator の実装
ActionCreatorはActionを返り値にとる以外はただの関数であり、Reduxとは関係がない。それを踏まえて実装しよう。
// action creators
const incrementCounter = () => {
// insert your logic here
return { type: 'INCREMENT' }
}
const decrementCounter = () => {
return { type: 'DECREMENT' }
}
const incrementCounterIfOdd = (val) => {
if (val % 2 !== 0) {
return { type: 'INCREMENT' }
} else {
return null
}
}
const incrementCounterAsync = () => {
return new Promise((res,rej)=>{
setTimeout(()=>{
res({ type: 'INCREMENT' })
},1000)
})
}
1秒後にカウンタ値をインクリメントするActionCreatorは、ActionがresolveされるPromiseを返すように設計した。
Step#03: ActionCreator を dispatchと連携
ActionCreatorは単純にActionを作るだけで、dispatchはしてくれない。どこかしらで生成したActionをdispatch()
する必要がある。dispatch
が与えられた時にActionCreatorとdispatch
がどう連携するかを記述するのが、react-reduxでいうところの、mapDispatchToProps
だ。今回はreactもreact-reduxも使わないので、使用感を似せるためにこんな感じに propsを定義してみた。
// emulate mapDispatchToProps
const props = {
increment: ()=>{
store.dispatch(incrementCounter())
},
decrement: ()=>{
store.dispatch(decrementCounter())
},
incrementIfOdd: (val) => {
const action = incrementCounterIfOdd(val)
if (action) {
store.dispatch(action)
}
},
incrementAsync: () => {
incrementCounterAsync()
.then((action)=>store.dispatch(action))
}
}
Step#04: まとめると
こんな感じになった。
<!DOCTYPE html>
<html>
<head>
<title>Redux basic example</title>
<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
</head>
<body>
<div>
<p>
Clicked: <span id="value">0</span> times
<button id="increment">+</button>
<button id="decrement">-</button>
<button id="incrementIfOdd">Increment if odd</button>
<button id="incrementAsync">Increment async</button>
</p>
</div>
<script>
// create store
function counter(state, action) {
if (typeof state === 'undefined') {
return 0
}
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
const store = Redux.createStore(counter)
// subscribe to render on update
const valueEl = document.getElementById('value')
function render() {
valueEl.innerHTML = store.getState().toString()
}
render()
store.subscribe(render)
// action creators
const incrementCounter = () => {
// insert your logic here
return { type: 'INCREMENT' }
}
const decrementCounter = () => {
return { type: 'DECREMENT' }
}
const incrementCounterIfOdd = (val) => {
if (val % 2 !== 0) {
return { type: 'INCREMENT' }
} else {
return null
}
}
const incrementCounterAsync = () => {
return new Promise((res,rej)=>{
setTimeout(()=>{
res({ type: 'INCREMENT' })
},1000)
})
}
// emulate connect(mapDispatchToProps)
const props = {
increment: ()=>{
store.dispatch(incrementCounter())
},
decrement: ()=>{
store.dispatch(decrementCounter())
},
incrementIfOdd: (val) => {
const action = incrementCounterIfOdd(val)
if (action) {
store.dispatch(action)
}
},
incrementAsync: () => {
incrementCounterAsync()
.then((action)=>store.dispatch(action))
}
}
// add lister to buttons
document.getElementById('increment')
.addEventListener('click', function () {
props.increment()
})
document.getElementById('decrement')
.addEventListener('click', function () {
props.decrement()
})
document.getElementById('incrementIfOdd')
.addEventListener('click', function () {
props.incrementIfOdd(store.getState())
})
document.getElementById('incrementAsync')
.addEventListener('click', function () {
props.incrementAsync()
})
</script>
</body>
</html>
Step#05: 所感
うまくやれば、非同期処理があっても redux-saga いらなくない? mapDispatchToProps
に非同期処理をごちゃごちゃ書けばいいだけの気がする。
Step#06: もっといじりたい!
Reduxでは、たくさんのExampleがあって、それをブラウザ上で編集できる神機能がある。
上記にあるいくつかのExampleに「check out the sandbox.」という文字がある。ここをクリックすると、オンラインでのIDEが開き、その場でファイルの修正と修正後の動作確認ができる。Codesandbox.io というサービスを利用しているようだ。