Posted at

Reduxが理解できない!?それなら作って理解しよう!


⚠️注意

記事内ではReduxを作っていきますが、所々わかりやすさのために簡略化などをしています。ですが、Reduxを理解する手助けにはなると思います。また、この記事はcodesandboxや似たようなサイトを使っているという前提の元、Reduxを作っていきます。ご了承ください。

後、この記事には筆者の寒いネタが多分に含まれます。閲覧する際には十分注意してください😇

上記の内容が大丈夫な方は、リダックスして読んでいって下さいね~😊


世の中の解説記事は抽象的過ぎて分からなぁぁぁぁぁぁぁい!

でしょ?


このデータフローもよくわからなぁぁぁぁぁぁい!

でも、この記事を読むと世界は変わる。データフローの意味が分かっちゃうんです!!! オオー >ΩΩ

さぁ、あなたもこの記事と一緒にReduxを作って


Redux だ~い好き💖

って言えるようになりましょう!


実際に作ってみた⛲

それでは、これからReduxを作っていきます。


下準備

まず初めにすることは、Reduxを作るための下準備です。

Reduxを環境にインストールして、公式サイトからサンプルコードを パクっ( お借りしましょう。

お借りしたサンプルコードへのリンク


src/index.js

// サンプルコードは分かり易さのため一部改変しています。ご了承ください

// codesandboxを使っている場合は、src/index.jsに張り付ける

import { createStore } from 'redux'

function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}

let store = createStore(counter)

store.subscribe(
function() {
console.log( store.getState() )
}
)

store.dispatch({ type: 'INCREMENT' }) // 1
store.dispatch({ type: 'INCREMENT' }) // 2
store.dispatch({ type: 'DECREMENT' }) // 1


この時、コンソール上に1, 2, 1と素数が表示されていればOK👌です ここツッコミどころですよ?

また、この挙動がReduxの正しい挙動だという事を海馬に叩き込んどいてください。ゼンソクゼンシンDA!

次に、import { createStore } from 'redux'と書いてある所を削除します。

すると、コンソール画面に

> ReferenceError: createStore is not defined

というエラーが発生すると思います。これで、下準備は完了です!

次に行きましょう👉

補足 : Reduxを作るとは具体的に何を作ることなのか?

ここまでで、Reduxを作るとは言っていますが、具体的に何を作るのかには触れていませんでした。

実はReduxを作るという事は、createStoreという関数を作るということに他なりません。

っと言いたいところですが、これには間違いが含まれていることをここで言っておきます。

ReduxにはapplyMiddlewareというReduxを拡張できる関数や他の便利な関数などが用意されており、createStore関数だけがReduxの全てではないということです。

ですが、createStore関数Reduxの大本であることは確かなので、この記事ではこのcreateStore関数のみに焦点を当てて作っています。






createStore関数を作成する

ReferenceErrorを出した所で下準備が完了しましたが、

「 なぜエラーを出したのか? 新手の詐欺か? 」

という疑問が出てくると思います。

その疑問にお答えすると、、、


Ω<「 この "ReferenceError" こそがReduxを作る最大のヒントだったんだよ!!!

Ω ΩΩ<『 な、なんだってー!? 』


という事で、このReferenceErrorを読み解いて人類滅亡を阻止しましょう🙄

ReferenceErrorの内容をヴォイニッチ手稿を読み解いた私が読み解くと、


ReferenceErrorを日本語訳した結果

> 参照エラー : createStore が定義されてないですよ?頭大丈夫ですか??


という事を言っています。ムカつきますね😡

これを解消するには、createStoreを定義します。ソースコードを以下のように修正してください。


上記のサンプルコードにcreateStore関数を追加

// ここにcreateStore関数を追加

// 下のサンプルコードからcreateStore関数は第一引数に関数を受け取ることが分かる
function createStore( reducer ) {
}

// 以下は "一切" 修正しない

function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}

let store = createStore(counter)

store.subscribe(
function() {
console.log( store.getState() )
}
)

store.dispatch({ type: 'INCREMENT' }) // 1
store.dispatch({ type: 'INCREMENT' }) // 2
store.dispatch({ type: 'DECREMENT' }) // 1


するとエラー内容が変わったと思います。見てみましょう👀

> TypeError: Cannot read property 'subscribe' of undefined

> 型のエラー: 未定義のプロパティ'subscribe'を読み取れません ( グルグル翻訳 )

と書かれています。とっっっても分かり易いですね😊

つまり、ソースコードの


上記のコードから抜粋

store.subscribe(

function(){
console.log( store.getState() )
}
)

の部分でエラーが発生していて、

エラーの理由は、store変数がundefinedになっているからという事ですね! サスガグルグルセンセイノホンヤクダ1

ReferenceErrorを読み解くと、このTypeErrorが出てきたので、これが次のヒントになりそうですね。

という事で、次に行ってみましょう👉


Storeオブジェクトを作る

前の節のTypeErrorを解消すべく、次はstore変数がundefinedにならないようにしましょう。

ソースコードを見てみると以下の処理が書いてあります。

let store = createStore(counter)

上記の処理を見るに、store変数の中身はcreateStore関数返り値のようです。

しかし、現在のソースコードにはcreateStore関数の中にreturn文が書かれていません。

これはJavaScriptでは undefinedを返り値とする! 」という事になります。

これがstore変数undefinedになってしまった原因ですね。

なので、TypeErrorにならないような値を返り値として設定してあげれば良さそうですね。

という事で、以下のようにソースコードを修正してください。


createStore関数の返り値を修正

function createStore( reducer ) {

// 返り値をsubscribe, dispatch関数を持つObjectに修正
return {
subscribe: function (cb) {}, // store.subscribeより返り値はsubscribeを持つオブジェクトだとわかる
dispatch: function (action) {} // store.dispatchより返り値はdispatchを持つオブジェクトだとわかる
}
}

// -- 省略 --


しれっとdispatch関数が定義されていますが、subscribe関数と同じようなエラーが出るので、まとめて定義しちゃいました。まぁ、敬虔なJavaScript狂信者の皆さんなら大丈夫ですよね?

ここまでの修正で、コンソール画面上にはエラーが表示され無くなったと思います。

これで、人類滅亡という問題( エラー )は解決されましたが、ソースコードを見てみると関数の定義ぐらいしか書いておらず、Reduxを「 つくったぁ〜〜〜!? 」とは言えなさそうです。

ここまでコンソール上にでぇ~たぁ~Errorがヒントとなっていたので、Errorが出なくなった今、また別のヒントを探す🔍必要がありそうです。

そうして、次なるヒントを求め我々は人を丸呑みするという呪われた竜の使いヅォン・ドゥーの住まうジャングルへと足を踏み入れたのだった。。。

次に行きます👉


ヒントをたずねて三千里

😣 <「 クソ!ヒントが無くなった!これから一体どうやってReduxを作ればいいんだ! 」

🤔 <「 はぁー、何か他にヒントになりそうなモノは無いのか ? 」

👩 < 「 コ〇ン君、借りてきたサンプルコードって、1,1,1って表示されてれいばいいの? 」

😒 < 「 バーロー、1,2,1って言ってるだろ。。。ん? 」

ピコリン Σ😲 <「 そうか!もう既に答えは出ていたんだ! 」

という事で、実はもう既にRedux作成の答えは出ていました。

それは、import { createStore } from 'redux'を書いていた時に、コンソールに出ていた1,2,1というReduxが残したダイイングメッセージです。

どういうことか整理すると、

このダイイングメッセージが、Reduxの正しい挙動である事は前の節に言いました。なので、このダイイングメッセージと同じ挙動するモノを作れば、見かけ上Reduxと同じなので、Reduxを作ったといえるのです。

よって、現時点のソースコードを修正して、コンソール画面に1,2,1と表示された時が私たちのゴールとなります。

※ 雑なまとめ方ですが、細かいところは気にしないでください。

答えが分かれば、次に何をすればいいのか分かります。

コンソール画面に1,2,1と表示するには、console.logという関数が必要なので、importしていた時に1,2,1と表示されていたのであれば、何処かにconsole.logが書かれているはずです。なので、console.logが書かれたところを探すと、、、

ありました!


console.logが書かれているところを抜粋

store.subscribe(

function() {
console.log( store.getState() )
}
)

ちゃんとconsole.logが書かれていますよね?

という事は、次のヒントはconsole.logが書いてある関数を受け取っているsubscribe関数にあるわけです。

次に行きます👉


subscribe関数を実装する

次のヒントがsubscribe関数に有りそうなのは分かりましたが、subscribe関数に何を書いたら良いのでしょうか?

とりあえず、Reduxの残してくれたダイイングメッセージをもう一度見てみたいので、subscribe関数に渡したconsole.logが書いてある関数を実行してみましょう。


ソースコードを以下のように修正

function createStore( reducer ) {

return {
subscribe: function (cb) {
cb(); // console.logを内部で実行している関数を実行している
// 要するに、console.logを実行している
},
dispatch: function (action) {}
}
}

// -- 省略 --


すると、あら不思議!ねればねるほど、コンソール画面にTypeErrorが!🧙‍♀️

> TypeError: store.getState is not a function

これには、成美ちゃんもびっくり!!! 👧 <「 マジビックリナンダケド 」

ここまで付いてこれた皆さんならもう大丈夫ですね?

store.getStateが関数じゃない! 」と怒られているので、前と同じようにgetState関数を定義しましょう!


ソースコードにgetState関数を追加

function createStore( reducer ) {

return {
subscribe: function (cb) {
cb();
},
dispatch: function (action) {},
getState: function(){} // ここにgetState関数を追加しました!
}
}

// -- 省略 --


すると、コンソール画面にundefinedが表示されました。どうやらconsole.log( store.getState() )の処理はちゃんと実行されているようですね。しかし、期待していたものとは大きく違います。

期待したのは1,2,1と数字が3回表示されることです。

しかし、


  1. 表示されたのは一回だけ

  2. 数字ではなくundefindが表示される。

という結果になりました。現状のソースコードだと上記の二つのバグがあるのでそこを修正しますが、修正箇所はソースコード全体を見るとすぐに分かります。


現時点でのソースコード全体

function createStore( reducer ) {

return {
subscribe: function (cb) {
cb();
},
dispatch: function (action) {},
getState: function(){}
}
}

function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}

let store = createStore(counter)

store.subscribe(
function() {
console.log( store.getState() )
}
)

store.dispatch({ type: 'INCREMENT' }) // 1
store.dispatch({ type: 'INCREMENT' }) // 2
store.dispatch({ type: 'DECREMENT' }) // 1


全体を見ると、subscribe関数は一回しか実行していませんが、dispatch関数3回も実行されているではあ~りませんか!

この 3 という数字。何か引っかかりますね。

subscribe1回 Actionは2種類 3時のおやつはdispatch~♪

そうですね! 私たちが求めていたのは、3回数字を表示する事でしたね!という事は、このdispatch関数の中で、subscribe関数に渡された関数を実行するようにすれば、3回表示する事が出来そうです。

以下のように修正してください。

function createStore( reducer ) {

const callbackList = [];

return {
subscribe: function (cb) {
callbackList.push( cb ); // コールバックを配列に追加
// いくつでもコールバックを設定できるように配列にしてます
},

dispatch: function (action) {
callbackList.forEach(
function( cb ) { // subscribeで追加したcallbackを全て実行
cb();
}
);
},

getState: function(){}
}
}

// -- 以下省略 --

上記のように修正すると、コンソール画面にundefindが3回表示されたと思います。

これで、表示回数は同じになりました。残る問題は、インドの山奥で数字を表示する事です。

次に行きましょう👉


getState関数を実装する

残る問題は、数字を表示する事です。

これを解決するには、表示している所を見れば直ぐに分かります。


表示部分を抜粋

console.log( store.getState() )


上記のコードを見るに、console.log関数store.getState関数の返り値を表示しています。

という事は、未だ何も書いていないgetState関数を実装すればいいわけです。

以下のように修正しましょう。

function createStore( reducer ) {

const callbackList = [];

return {
subscribe: function (cb) {
callbackList.push( cb );
},

dispatch: function (action) {
callbackList.forEach(
function( cb ) {
cb();
}
);
},

getState: function () {
return 0; // ここを修正しました!
}
}
}

// -- 以下省略 --

すると、コンソール画面に0が三回表示されたと思います。

これで、残る問題も解決できましたね。いや~、めでたし、めでたし🤗っとなれば良かったのですが、まだ地獄は続きます👹

getState関数は、常に0を返しています。これでは、いくら実行してもカロリー0です👌

夢のようではありますが、私たちは1,2,1と表示したかったので、このままでは駄目です。主に体重が

なので、より柔軟にするために変数を使って実装してみましょう。以下のように修正します👇

function createStore( reducer ) {

const callbackList = [];
let currentState = 0; // 値を変更できるように "let" を使い、初期値に "0" を設定

return {
subscribe: function (cb) {
callbackList.push( cb );
},

dispatch: function (action) {
callbackList.forEach(
function( cb ) {
cb();
}
);
},

getState: function () {
return currentState; // 変数を使うように修正しました!
}
}
}

// -- 以下省略 --

これでcurrentState変数を変更すれば、getState関数が返す値を変更することができますが、今はまだ変更する処理を書いていませんので、0しか表示されてねぇ! 」って感じになっています。

なので、次に変更する処理を書いてきましょう!ちくわ大明神

次に行きましょう👉


dispatch関数を実装する

さて、ここからはcurrentStateを更新する処理を書いていくわけですが、ちょっと此処でもう一度データフローを見てみましょう👀

上記のデータフローの画像を見て、Dispatchした後に、ReducerActionを渡してStoreを更新しています。

だんだん、分かってきましたね。つまり、dispatch関数を実行すると上記のような事が起こるわけです。

という事で、このデータフローを実装しましょう!


Reducerに入らずんばStateを得ず

先人は良く言ったものですね😊

では、dispatch関数を以下のように修正してください。

function createStore( reducer ) {

const callbackList = [];
let currentState = 0;

return {
subscribe: function (cb) {
callbackList.push( cb );
},

dispatch: function (action) {
currentState = reducer( currentState, action ); // reducerにStateとActionを渡して実行
// reducerの返り値は次のStateになるので、currentStateにそのまま代入
callbackList.forEach(
function (cb) {
cb();
}
);
},

getState: function () {
return currentState;
}
}
}

// -- 以下省略 --

ちょっと色々と書いてしまったので、dispatch関数に実装してる内容を整理してみましょう。

まずは、以下の部分


dispatch関数から抜粋

currentState = reducer( currentState, action ); 


これは、データフローで言う所の Dispatch -> Reducer -> Store のところです。

Dispatch -> Reducerの部分でDispatchは、ReducerActionを渡していましたが、それはreducer関数の第二引数に渡すことで実装しています。

また、Reducerを実行すると新しいStateが返ってくるので、それをcurrentStateに代入しています。

これがReducer -> Storeの部分になります。

そうして次に、以下の部分を見てみましょう

callbackList.forEach(

function( cb ) {
cb();
}
);

forEach関数を使っていて、ちょっと分かりにくいかもしれませんが、やっていることは単純です。

配列に保存されている関数をループして実行してるだけです。

また、ここはデータフローで言う所のStore -> Viewのところです。

つまり、あなたがパソコンを開いて何処の誰とも知らぬおっさんの声でYou've Got Mail!とメールが来たことを知らされた時と同じように、コールバックを実行することでReduxを使っている人(View)に変更の通知を送っているのです。


なんちゃってReduxの完成!

はい!という事で、今、コンソール画面を見てみるとそこにはReduxが残してくれたダイイングメッセージと同じものが表示されていると思います!

前の節で1,2,1とコンソール画面に表示されていれば、実質Reduxを作ったと同じだと言っていましたので、

ついに私たちはReduxを作ることができました🎉🎉 ヤッタネー

Reduxを作ったことを確認するためソースコード全体をあらためて見てましょう!


完成したソースコード全体

function createStore( reducer ) {

const callbackList = [];
let currentState = 0;

return {
subscribe: function (cb) {
callbackList.push( cb );
},

dispatch: function (action) {
currentState = reducer( currentState, action );

callbackList.forEach(
function( cb ) {
cb();
}
);
},

getState: function(){
return currentState;
}
}
}

function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}

let store = createStore(counter)

store.subscribe(
function() {
console.log( store.getState() )
}
)

store.dispatch({ type: 'INCREMENT' }) // 1
store.dispatch({ type: 'INCREMENT' }) // 2
store.dispatch({ type: 'DECREMENT' }) // 1


お疲れさまでした😆 ここまで辛く極寒地獄のような文章を読み終えたあなたならきっと上記のソースコードを理解でき、Reduxについて理解できていると思います。そう願いたい

ですが忘れないでください。これがちゃんとしたReduxではないという事に。。。


俺たちはようやくのぼりはじめたばかりだからな このはてしなく遠いRedux坂をよ…


👈To BE CONTINUED…🎸


後書きみたいな何か

最近はフロントエンドも落ち着いて、穏やかな時の流れを感じるようになり、これをフロントエンドの進化と歓迎する者もいるとかいないとか。。。

まぁそんなことは置いといて、この記事には無駄にネタが多く含まれているので、この記事を読んでもReduxを理解できない人が大半だと思います。そんな人たちは「うんうん、それもまたアイカツだね。」ってことで許してください🙇‍♂️

逆に理解できた人は、優秀なエンジニアさんだと思います。そんな優秀なエンジニアさんならきっと、Reduxよりも良いライブラリを作ってくれることでしょう 😁<「 市民、良いライブラリを作るのは義務ですよ? 」


この記事が本当に伝えたい事

実は、この記事で伝えたかった事はReduxの概念でも、仕組みでも、筆者の面白いギャグでもありません。

伝えたかった事は、Reduxはとてもシンプルに実装されているという事です。

世の中にあるフレームワークライブラリは、一見すると難しい事をしているように見えますが、実は簡単な事しかやってなかったりします。なので、たまには使っているフレームワークライブラリの中を覗いてみて下さい。

きっと、あなたが思っているほど難しいことはしてないかもしれませんよ?

そして、もしソースコードが理解できたなら、そのプロジェクトに参加して貢献してみてはいかがでしょうか?

ここまで読んでくださりありがとうございます!

何か質問などがあればお気軽にコメントしてください😆

それでは、世に平穏のあらんことを👋 ガラムマサラ