Redux Toolkitのミドルウェアが分かりにくい・・・。

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);

次の関数に渡す引数を細工したり、

いろいろ応用が利きそう。

こういった使い方、慣れるまで大変そう。

BlockEditor certificate css DataGrid Docker Gutenberg Hyper-V iframe MUI openssl PHP React ReduxToolkit REST ubuntu WordPress オレオレ認証局 フレームワーク