Redux Toolkitの学習中、ミドルウェアがちょっと分かりにくい。
関数が関数を返すまでは理解しやすいけど、さらに関数を返すとなると理解が追いつかない。
Cのポインタのポインタのポインタみたいな?
Reduxの内部は一切理解してませんが、それに近いものをどうやって作るか思考錯誤しました。
なので間違ってるかもしれませんのであてにはしないでください。
まずRedux Toolkitでミドルウェアを書いた場合。
export const aMiddleware: Middleware = store =>
{
console.log('<a-store>');
return next =>
{
console.log(' <a-next>');
return action =>
{
console.log(' <a-action>');
return next(action);
}
}
}
export const bMiddleware: Middleware = store =>
{
console.log('<b-store>');
return next =>
{
console.log(' <b-next>');
return action =>
{
console.log(' <b-action>');
return next(action);
}
}
}
export const cMiddleware: Middleware = store =>
{
console.log('<c-store>');
return next =>
{
console.log(' <c-next>');
return action =>
{
console.log(' <c-action>');
return next(action);
}
}
}
export const store = configureStore({
reducer: {
// 略
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(aMiddleware, bMiddleware, cMiddleware)
});
ここで適当なアクションを3回実行するとこうなる。
<a-store>
<b-store>
<c-store>
<c-next>
<b-next>
<a-next>
<a-action>
<b-action>
<c-action>
<a-action>
<b-action>
<c-action>
<a-action>
<b-action>
<c-action>
ミドルウェアの一番外側と二番目が1回実行される。
後はアクション毎に内側が実行される。
一番外側がstoreを保持する用、
二番目が関数を連鎖させる用、
内側がアクション毎に実行される用
みたいな感じになってる。
最低限の連鎖
function a(next: any)
{
console.log('A');
return () => next(); // aaは bb()を実行
}
function b(next: any)
{
console.log('B');
return () => next(); // bbは cc()を実行
}
function c(next: any)
{
console.log('C');
return () => next(); // ccは exec()を実行
}
const exec = () => console.log('Hello World');
const func = a(
b(
c(
exec
)
)
);
console.log('--- func 1 ---');
func();
console.log('--- func 2 ---');
func();
C
B
A
--- func 1 ---
Hello World
--- func 2 ---
Hello World
まずfunc
を作成するにあたり、c() -> b() -> a()
の順に実行されるのでC B A
の順に表示されている。
ではa()の戻り値func
を実行すると、Hello World
が表示される。2度実行しているので2回表示される。
この流れを考えると、
- execは関数
- c()はその引数(exec)を実行する関数(ccとする)を返す関数
- b()はその引数(cc)を実行する関数(bbとする)を返す関数
- a()はその引数(bb)を実行する関数(aaとする)を返す関数
- funcを実行すると、
aa() -> bb() -> cc() -> exec()
の順で実行される。
頭の中で想像しよう。
ちょっと分かりにくかったらログを表示して流れを追っていく。
さらに戻り値も返していく。
戻り値と連鎖
function a(next: any)
{
console.log('A');
return () =>
{
console.log('exec A');
return next();
}
}
function b(next: any)
{
console.log('B');
return () =>
{
console.log('exec B');
return next();
}
}
function c(next: any)
{
console.log('C');
return () =>
{
console.log('exec C');
return next();
}
}
const exec = () =>
{
console.log('Hello World');
return 123;
}
const func = a(
b(
c(
exec
)
)
);
console.log('--- func 1 ---');
const result1 = func();
console.log(`RESULT = ${result1}`);
console.log('--- func 2 ---');
const result2 = func();
console.log(`RESULT = ${result2}`);
C
B
A
--- func 1 ---
exec A
exec B
exec C
Hello World
RESULT = 123
--- func 2 ---
exec A
exec B
exec C
Hello World
RESULT = 123
まずfunc()
を実行するとexec A exec B exec C
の順に実行されていることがわかる。
またexec()
は戻り値123
を返しているため、func()
の戻り値は123
になる。
func()
の流れはaa() -> bb() -> cc() -> exec()
こう見ると分かりやすい。
引数を追加してみる
function a(next: any)
{
console.log('A');
return (data: any) =>
{
console.log('exec A');
data['aaa'] = 'AAA';
return next(data);
}
}
function b(next: any)
{
console.log('B');
return (data: any) =>
{
console.log('exec B');
data['bbb'] = 'BBB';
return next(data);
}
}
function c(next: any)
{
console.log('C');
return (data: any) =>
{
console.log('exec C');
data['ccc'] = 'CCC';
return next(data);
}
}
const exec = (data: any) =>
{
console.log('Hello World');
data['hello'] = 'world';
return data;
}
const func = a(
b(
c(
exec
)
)
);
console.log('--- func 1 ---');
const result1 = func({});
console.log(result1);
console.log('--- func 2 ---');
const result2 = func({});
console.log(result2);
C
B
A
--- func 1 ---
exec A
exec B
exec C
Hello World
{aaa: "AAA", bbb: "BBB", ccc: "CCC", hello: "world"}
--- func 2 ---
exec A
exec B
exec C
Hello World
{aaa: "AAA", bbb: "BBB", ccc: "CCC", hello: "world"}
func({})
で空のリテラルオブジェクトを渡している。
と同時にプロパティを追加している。
aa({}) -> bb({aaa: "AAA"}) -> cc({aaa: "AAA", bbb: "BBB"}) -> exec({aaa: "AAA", bbb: "BBB", ccc: "CCC"})
aa()
の中でaaa
を追加して、
bb()
の中でbbb
を追加して、
cc()
の中でccc
を追加して、
exec()
の中でhello
を追加して・・・。
function a(next: any)
{
console.log('A');
return (data: any) =>
{
console.log('exec A');
console.log({ ...data }); // ←追加
data['aaa'] = 'AAA';
return next(data);
}
}
こんな感じで引数を表示すると分かりやすい。
ミドルウェアの一番外側を再現?
今回は一番外側を再現します。
ただ複雑になるので一番内側のコードは引数の無いバージョンにしました。
const state = {
aaa: 'AAA',
bbb: 'BBB',
ccc: 'CCC'
};
type IState = typeof state;
function createA(store: IState)
{
console.log(`--- ${store.aaa} ---`);
function a(next: any)
{
console.log('A');
return () => next();
}
return a;
}
function createB(store: IState)
{
console.log(`--- ${store.bbb} ---`);
function b(next: any)
{
console.log('B');
return () => next();
}
return b;
}
function createC(store: IState)
{
console.log(`--- ${store.ccc} ---`);
function c(next: any)
{
console.log('C');
return () => next();
}
return c;
}
const exec = () => console.log('Hello World');
const a = createA(state);
const b = createB(state);
const c = createC(state);
const func = a(
b(
c(
exec
)
)
);
console.log('--- func 1 ---');
func();
console.log('--- func 2 ---');
func();
--- AAA ---
--- BBB ---
--- CCC ---
C
B
A
--- func 1 ---
Hello World
--- func 2 ---
Hello World
一番外側の関数でストアの代わりに、ただのリテラルオブジェクトを渡してます。
流れを見るとミドルウェアの初期化のされかたが一致してる。
あと一歩、
最後に一番内側の流れをよく確認します。
const state = {
aaa: 'AAA',
bbb: 'BBB',
ccc: 'CCC'
};
type IState = typeof state;
function createA(store: IState)
{
function a(next: any)
{
return (data: any) =>
{
console.log('A');
data['aaa'] = store.aaa;
return next(data);
}
}
return a;
}
const createB = (store: IState) => (next: any) => (data: any) =>
{
console.log('B');
data['bbb'] = store.bbb;
return next(data);
}
const createC = (store: IState) => (next: any) => (data: any) =>
{
console.log('C');
data['ccc'] = store.ccc;
return next(data);
}
const exec = (data: any) =>
{
console.log('Hello World');
data['hello'] = 'world';
return data;
}
const a = createA(state);
const b = createB(state);
const c = createC(state);
const func = a(
b(
c(
exec
)
)
);
console.log('--- func 1 ---');
const result1 = func({});
console.log(result1);
console.log('--- func 2 ---');
const result2 = func({});
console.log(result2);
--- func 1 ---
A
B
C
Hello World
{aaa: "AAA", bbb: "BBB", ccc: "CCC", hello: "world"}
--- func 2 ---
A
B
C
Hello World
{aaa: "AAA", bbb: "BBB", ccc: "CCC", hello: "world"}
createA()
は分かりやすさのための書き方をしてますが、createB()
とcreateC()
はスッキリさせました。
まとめ
next()
は次の関数を実行、さらに次の関数を実行、と連鎖するため。
return next(data) * 2
next()
の戻り値を細工したり、
return next(data * 2);
次の関数に渡す引数を細工したり、
いろいろ応用が利きそう。
こういった使い方、慣れるまで大変そう。