👻

ライブラリゼロの20行でReduxもどきを実装して、Reduxを完全掌握しよう!!

2022/05/24に公開約11,600字1件のコメント

はじめに

Reduxは遠い昔に誕生したものなので、いまReduxを使っていない人も多いかもしれません。

Reduxは、出現当時はそれほど大きなソフトウェアではなかったのですが、ときが経つにつれて、いろいろな便利関数たちが現れてきて、そのせいで今からReduxを調べる人は、何が本質なのかを調べるのが難しくなっていると思います。

そこで3分でReduxもどきを実装しました。こちらです。20行!! じゃーん! JavaScriptでうごきます!!

// 実装
const createStore = (reducer) => {
  let state = 0;
  const listeners = [];

  const dispatch = (action) => {
    const newState = reducer(state, action);
    state = newState;
    listeners.map(listener => listener());
  }

  const subscribe = (listener) => {
    listeners.push(listener);
  }

  return {
    dispatch,
    getState: () => state,
    subscribe,
  }
}

// 以下は使い方
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment': {
      return state + 1;
    }
  }
}

const store = createStore(reducer);

const logger = () => {
  console.log(store.getState());
}

store.dispatch({ type: 'increment' })
store.subscribe(logger);

store.dispatch({ type: 'increment' }) // -> 2
store.dispatch({ type: 'increment' }) // -> 3
store.dispatch({ type: 'increment' }) // -> 4

(説明はあとで書きますので、眺めたあとはスキップしていただいてだいじょうぶです。こちらはTL;DRになっております。)

歴史!!! 昔はつらかったJavaScript

かんたんに昔のJavaScriptのころの話をします。理解の補助になるからです。

レガシーJSはつらかった話(長いです。わかりにくいと思ったので中に閉じこめました。読まなくてもいいです)

そもそも、いったいどうしてReduxなどというものが誕生したのでしょうか。

なのでレガシーJSのワールドのお話をしたいと思います。

むかし、JavaScriptで状態を管理するときは、コンポーネントにその状態を直に書いていました。

ちょうどこのようなノリです。

<div data-foo="mydata" id="foodiv" />

<script>
  document.getElementById('foodiv').dataset['foo'] = 'new data!'
</script>

そして、コンポーネントの状態を変更するときは、ここをいろいろな場所から変えていたのです。

document.getElementById('foodiv').dataset['foo'] = 'action1を実行したあとの状態!'
document.getElementById('foodiv').dataset['foo'] = 'action2を実行したあとの状態!'
document.getElementById('foodiv').dataset['foo'] = 'action3を実行したあとの状態!'

注意点ですが、この話では、document.getElementById などは重要ではないです。そういう話ではなく、いろんな箇所から状態変更を呼び出しまくっていたので、誰がいつどこから状態を変更していたのかわからなくなっていました。

これは3つなのでたいしたことがないのですが、こういうものがたくさんあったので、意味不明でした。

また、値が変更されたときに、別の変更をしなければなりませんでした。つまりこういうことです。

<div data-foo="mydata" id="foodiv" />
<div data-foo="違うdata" id="bardiv" />

<script>
document.getElementById('foodiv').dataset['foo'] = 'new data!'
document.getElementById('bardiv').dataset['foo'] = 'foodivを変えたときはこっちも変えないといけない'
</script>

たくさん状態変更しようとするとこうなります。

document.getElementById('foodiv').dataset['foo'] = 'actionを実行したあとの状態!'
document.getElementById('bardiv').dataset['foo'] = 'foodivを変えたときはこっちも変えないといけない'
document.getElementById('foodiv').dataset['foo'] = 'actionを実行したあとの状態!'
document.getElementById('bardiv').dataset['foo'] = 'foodivを変えたときはこっちも変えないといけない'
document.getElementById('foodiv').dataset['foo'] = 'actionを実行したあとの状態!'

おおっと? これを見ると、action3.jsだけbardivの変更が書かれていませんね。これはバグです。そう、昔はこういうバグが多かったのです!!!(ぼく調べ)

このように、何かの状態を変更するときは、変更したいときはそのアクション自体(状態を変更すること)の他に、別のアクションはどうするべきかをずっと考えないといけなかったのです。

これは言うならば、「牛丼を食べにお店に入る」というように書くところで、

牛丼を食べにお店に入り、
店員が元気よく挨拶されたら自分は『並一丁!』と言って、
店員が元気よくオーダーを受けて、
おだやかな気持ちで微笑みながら、
私は席に座る

とまで書かないとバグる、というような状況です。健全ではありません。

そうではなくて、

- 「牛丼を食べにお店に入る」というアクションを起こす
- 牛丼屋で反応されたら、『並一丁』と言う
- 元気のいい店員さんがいたときは、おだやかな気持ちになる
- 注文が終わったら、席に座る

と書きたいですよね??これをReactive Programmingと言います。なにかに反応して動作するプログラミングの技法です。昔流行りました。

コードでいうなら、ちょうどこのような形で書きたいのです。ちなみにこれは擬似コードなので動きません。

<div id="foodiv" />
<div id="bardiv" />

<script>
なんかいろいろな状態が置いてある場所['foo'] = 'actionを実行したあとの状態!';

bardiv.addEventListener("なんかいろいろな状態が置いてある場所['foo']のonChange", () => {
  なんかいろいろな状態が置いてある場所['bar'] = 'foodivを変えたときはこっちも変えないといけない';
})
</script>

こうすることができれば、divタグの方では、使いたいときになんかいろいろな状態が置いてある場所を呼べばいいことになります。

昔、JavaScriptがつらかったときに、Reactive Programmingというのが流行りました。

Reactive Programmingは、名前からしてかっこいいです。かっこいいものは流行ります。

「何らかの動作によって状態が変わると、それにReactiveに反応して別の動作を起こす」というのがReactiveなところの特徴です。ドッキリにかけられたリアクション芸人みたいなイメージです。

もしかしたら、これはふつーーのように聞こえておもしろくないかもしれませんが、プログラミングの世界では、意外とそんなにふつーーではないです。

プログラミングでは、ぼくたちは絶えずアクション(動作)を書かされます。

「動作して動作して動作する」みたいなことを書かされます。ずっと。

それも別にいいのですが、ときどき「別に動作のことは知らないけど、まあとにかくこういう状態のときはこうなってよ」と書きたいときがあります。

たとえばおやつの棚があったとして、別に誰がおやつ棚におやつを補充するとか、おやつは誰が買うとか、そんなこと関係なく「おやつ棚におやつがあったら食べる」ってなりたいですよね。

そんな感じのことをJavaScriptでもしたかったのです。

React と react-redux と Redux

もしかすると勘違いされている方がいるかもしれないので、ここでことわっておきたいと思います。

ReactとReduxは関係ありません。まったく別のものです。ReactとReduxをつなぐものがreact-reduxです。

すべて別のものです。

「ハム」「ハムスター」「ハム&ハムスター」ぐらい違います。

Reduxの話をしてるのに、Reactがうんたらと言われたら、詐欺だと思ったほうがいいです!

Reduxはこのようにして使う!

つまり、Reduxとはさっきのかっこいいこと(Reactive Programming)をしたいわけです。

実際にReduxを使うときにはこのように書きます。

store.subscribe(() => {
  const state = store.getState()
  (state['店員のあいさつ'] === '元気がいい') && console.log('私は喜ぶ!')
})

const action = {
  type: "牛丼を注文する"
}
store.dispatch(action)

このような感じです。 console.log('私は喜ぶ') だけ、先程の例と違いますが(∵ 書くのがめんどくさかった)、おおむねこのような感じです。

このように書くと、もしかしたらわかりにくく感じるかもしれません。とくに、コードに「牛丼」だなんて書かれると、具体的すぎてわかりにくいですよね。

でも今はぼくを信じてください。store / action / dispatch / subscribe、の4つの英単語が少し怖いだけなので、安心してください。あとで説明します。

とにかく、「牛丼を注文する」「店員のあいさつの元気がいいときは、私は喜ぶ!」というふうに2つのことが分離できていて、それがとてもうれしいのです。

  • アクションを起こす!
  • もしほげほげの状態が変わったら、それに応じて別のアクションを起こしたりする!Reactive!

こういうふうなことができて大変便利です。Reduxはべんり。

とても重要なので、この2つだけ復唱していただけると、いいかもしれません。

Reduxを漸進的に理解していく

store -> dispatch -> action -> state -> subscribe -> reducer

の順に説明します。reducerは、伏線回収のようになっているので、最後だけ読むとなぞすかもしれません。上から順に読むことをオススメします。

Store

こちらの画像の円をご覧ください。これはRedux Storeと名付けられています。Storeというのは、データ置き場のことです。「Reduxくんのデータ置き場」みたいな感じです。

reduxでは、createStoreで作ります。そのまんまですね。

dispatch

そして、このstoreにデータをぶちこんでいきたいと思います。

実は、storeにdispatchという関数(store.dispatch)があって、それを外から使えるようになっています。dispatchは「〜を発送する」とか「〜を派遣する」とかという意味があります。

このdispatchを通じて、storeの中のデータ(状態のこと)を変更していくわけです。

action

このdispatchに、Actionを渡すことで、中のデータが変わってほしいのです。(store.dispatch(action))

Actionとはアクション(動作)のことです。難しく考える必要はありません。アクションです。映画撮影の「アクション!!!!!!」って言われるアクションをイメージしてください。

Actionは、JavaScript Plain Objectで書かれます。Plain Objectと言われると怖いかもしれません。しかしPlain Objectというのは、単なるJavaScriptのObjectのことです。

ヨーグルトのプレーン味と同じです。JavaScriptのArrayがバナナ味、Mapがイチゴ味みたいなノリです。

「な〜〜んだ、ヨーグルトのプレーン味か」とおもってください。JavaScriptのプレーンなObjectなのです。

Actionはこう書きます。

const action = {
  type: '牛丼を注文する'
}

action.typeは、英語じゃないとけしからんと思っている人もいるかもしれませんが、別に日本語でもいいじゃない、だって文字列リテラルなんだもの。

それをdispatchします。ノリ的には「これやったから、あとはよろしくね」というような委譲の感じです。

state

そしてデータが変更されます。「どのようにデータが変更されるのか???」は気にしなくていいです。気になる人のために言っておきますが、XXducerで変更されます。redXXXrはあとで書きます。

dispatch(action) をすると、storeにはstateという、データ置き場の中の真のデータ置き場の場所(つまり状態のこと)があるのですが、それが変更されるのです!

変更されたデータは store.getState() でとります。そのまま取らせないようにしてます。なんでそのまま取らせないようにしているかというと、こまけぇこたあいいんだよ(あまり詳しくない)

dispatch(action)しただけなのにstateが変更されるだなんて……いったい何ducerのしわざなんだ……???

subscribe

storeで変更されたstateはあとでsubscribeというところから呼び出します。subscribeは「〜を購読する」という意味です。

store.subscribe(listener) というように使います。もしかしたら図がわかりにくいかもしれませんが、YouTubeの「チャンネル購読おねがいします」をイメージするといいかもしれません。「リスナーを購読させる」という意味です。

実際のコードでは

store.subscribe(() => {
  const state = store.getState()
  (state['店員のあいさつ'] === '元気がいい') && console.log('私は喜ぶ!')
})

のようになります。とにかくstateが変わったあとのことをここに書きます。

これがすごく重要です。

  • 「こまけえこたあ知らないけど、なんか状態がこうなったらこの動作やるわ」
  • 「こまけえこたあ知らないけど、なんか状態がこうなったら、こっちの状態はこうなる」

というのが書けるのです。これがすごい大発見で、一大ムーブメントになったのです。

そして当時Reduxがわかる人はヒーローでした(しらんけど)

ちなみに、Reactは全然関係ないと言いましたが、もしかするとここでムズムズしている人がいるかもしれません。

「Reactからsubscribeなんて呼び出さないぞ!詐欺か!?」と。

実はそれはreact-reduxがやってくれています。react-reduxの <Provider />connect() が、全部やってくれているのです。ありがたいです。

reducer

最後にお待ちかねのReducerです。

reduce とは、「〜を減らす」という意味がありますが、その通り、減らすのです。とくに、プログラミング言語では、reduceはたくさんあるものを1つにするというようなイメージになります。

reducer(state, action) というように、2つの引数を入れて実行すると、1つのstateが返ってくるというイメージです。コードでは次のような感じです。

const reducer = (state, action) => {
  if (state === '注文されてない' && action.type === '牛丼を注文する') {
    return '注文されてる'
  }
  return '注文されてない'
}

図でいうとこうです。

このreducerというのは、storeを作るときにあらかじめ登録しておきます。createStore(reducer) と書きます。

まとめ

流れを振り返って

「actionをdispatchすると、storeの中でいい感じに(reducerで定義したとおりに)stateが変更されるから、あとはそれをsubscribeしておけばいいんだな〜〜〜〜」

というのが理解できてスラスラ言えれば完璧です!

あなたはReduxをマスターしました!

action creatorとかcomposeとかreselectとかselectorとかcombineReducersとかmiddlewareとかredux-sagaとかは???

それらはすべてオマケで、周辺概念です。Reduxは上に書いたことがわかれば、あとは周辺知識を肉付けしていく形となります。

つくってわくわくRedux

Reduxもどきを作ると理解がはかどるかもしれません。最初にあげたコードを1つ1つ棚卸ししていきます。

storeをつくる!

そのままです。

const createStore = (reducer) => {
}

stateをつくる!

さきほどの説明とは順序がかわりますが、先にstateを作ってしまいます。状態を変えたいから、状態を作ってしまいます。

const createStore = (reducer) => {
+ let state = 0;
}

初期値を0にしてますが、気にしないでください。まともに作ろうとすると大変なので、今回はReduxもどきを作るという考えで、数値が入っているだけのstateをつくることにします。

getState() を入れておきます。

const createStore = (reducer) => {
  let state = 0;
  return {
+   getState: () => state
  }
}

dispatchをつくる!

// ⚠: これは使う側のコードです
const action = {
  type: 'increment'
}

を使えるようにします。まずは↓のような感じで、関数を定義だけします。

const createStore = (reducer) => {
  let state = 0;
+ const dispatch = (action) => {}
  
  return {
    getState: () => state,
+   dispatch,
  }
}

中身を書いていきます。reducer(state, action) で新しいstate【newState】が返っていることを確認してください。このnewStateが新しいstateになります。(同語反復)

const createStore = (reducer) => {
  let state = 0;
  const dispatch = (action) => {
+   const newState = reducer(state, action);
+   state = newState;
  }
  
  return {
    getState: () => state,
    dispatch,
  }
}

reducerを登録してみる

この時点で、もうほぼ完成の片鱗が見えているので、一度reducerを作って登録してみるとワクテカすること間違いなしです。

// ここは変更なし
const createStore = (reducer) => {
  let state = 0;
  const dispatch = (action) => {
    const newState = reducer(state, action);
    state = newState;
  }
  
  return {
    getState: () => state,
    dispatch,
  }
}

// 以下を追加してます
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment': {
      return state + 1;
    }
  }
}

const store = createStore(reducer);
store.dispatch({ type: 'increment' })
store.dispatch({ type: 'increment' })
console.log(store.getState())  // -> 2

subscribeの実装

少し複雑かもしれません。しかし恐れることはないです。

subscribeするときに、listenerをstoreの中にあるlistenersに追加していって、

あとでdispatchしたときにそのlistenerを呼び出すという形です。

const createStore = (reducer) => {
  let state = 0;
+ const listeners = [];

  const dispatch = (action) => {
    const newState = reducer(state, action);
    state = newState;
+   listeners.map(listener => listener());
  }

  const subscribe = (listener) => {
+   listeners.push(listener);
  }

  return {
    dispatch,
    getState: () => state,
+   subscribe,
  }
}

そして自由に使います。

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment': {
      return state + 1;
    }
  }
}

const store = createStore(reducer);

const logger = () => {
  console.log(store.getState());
}

store.dispatch({ type: 'increment' })
store.subscribe(logger);

store.dispatch({ type: 'increment' }) // -> 2
store.dispatch({ type: 'increment' }) // -> 3
store.dispatch({ type: 'increment' }) // -> 4

そんな感じでした。Reduxはとてもたのしいです!

実装は20行なので、これを何も見ずにスラスラ書けると、あなたもReduxマスターです!

いかかでしたか?参考になった方はいいねをクリックしてください!

Discussion

説明がめちゃくちゃ分かりやすかったです!
記事を書いて頂きありがとうございます。

ログインするとコメントできます