【JavaScript】axios の通信キャンセルを使って最新の操作で起きた通信結果のみを得る

 キーボードを打ち込むたびなどの高頻度で何かするたびに外部と非同期で通信する際、使うと便利な機能である通信キャンセル機能の紹介です。

 ブラウザ上で非同期通信をしたい時、最後の1通信だけ有効となればいい場合がしばしばあります。それは例えばページネーションでページを次々と移動する、サジェスト検索で文字を打ち込むたびに検索する、といった場合です。この時、毎回通信を完了するとページ上が目まぐるしく変わる上、通信完了順によっては最後にブラウザ上から送った通信の結果が最終結果にならないときがあります。これを避けるために非同期通信そのもののキャンセルが望まれます。

 axios は JavaScript の非同期通信用ライブラリです、よくウェブページを開いたままAPIと通信するために使います。

Axios
axios/axios: Promise based HTTP client for the browser and node.js

 axios の中には通信キャンセル用の機能が備わっており、詳しくは次リンクにあります。
axios/axios: Promise based HTTP client for the browser and node.js#cancellation

 v0.22.0前後で使うべき機能が変わります。v0.22.0以降の場合は AbortController という仕組みを使う方がいいです。具体的には次の様に使えます。

// https://github.com/axios/axios#cancellation から引用
// 通信キャンセル用のインスタンスを用意
const controller = new AbortController();
// 通信キャンセル用のインスタンスと通信をつなげる
axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});

// 通信をキャンセルする
// cancel the request
controller.abort()

 これで通信をキャンセルできます。キャンセルした通信は次の様にキャンセル済みとなります。スクリーンショットはGoogleChrome上で撮りました。

 単独でのキャンセルならば axios のコード例そのままでよいのですが、想定するシーンは同じAPIを高速で連打される中で最後の一つだけ有効にするというパターンですので少々工夫が必要です。例えば素の JavaScript であれば次の様にすることができます。

document.addEventListener('DOMContentLoaded', function (){
    /** @var {AbortController|null} 現在通信中の axios をキャンセルするためのコントローラーを保持する変数 */
    let currentController = null;
    // ボタンをクリックする度に axios を走らせる
    document.getElementById('send-btn').addEventListener('click', function (){
        if(currentController){
            // 既存の通信が存在するならばここでキャンセル
            currentController.abort();
        }
        // 通信キャンセル用のインスタンスを用意
        const controller = new AbortController();
        // 既存の通信のコントローラーとしてこのイベント中の axios を制御する AbortController を変数に格納
        currentController = controller;
        // axios で通信
        axios.get('/tmp.php', {
            signal: controller.signal
        }).finally(function() {
            // 通信が完了したならば、キャンセル用のコントローラーを入れる変数を空にする
            currentController = null;
        });
    })
})

 どこかに通信中の axios を停止するための AbortController を用意しておき、通信の度にそこをチェックして、必要ならば通信をキャンセルします。

 Reactも方針的には同じなのですが、React自体の処理の重さなのかレンダリングアルゴリズムが原因なのか変数の中に一つコントローラーを保持するのみでは事故が起きやすくなるため、工夫が必要になります。具体的には次の様に配列に通信中コントローラーを格納し、常に全てを見ます。

    function App() {
        /** 
         * レンダリングに直接関係なく、速度重視のために ref を使用
         * この中に通信中のコントローラーを貯めていく
         */
        const currentControllerStack = React.useRef([]);
        const handleClick = () => {
            currentControllerStack.current = currentControllerStack.current.map(c =>{
                // 既存の通信が存在するならばここでキャンセル
                c.abort()
                return null;
            }).filter(c=>!!c);// キャンセル済みのコントローラーは null に変換して filter で削除

            // 通信キャンセル用のインスタンスを用意
            const controller = new AbortController();
            // 既存の通信のコントローラーとしてこのイベント中の axios を制御する AbortController を通信中コントローラー配列に追加
            currentControllerStack.current.push(controller);
            // axios で通信
            axios.get('/tmp.php', {
                signal: controller.signal
            }).finally(function () {
                // 通信が完了したならば、キャンセル用のコントローラーを通信中コントローラー配列から除去する
                currentControllerStack.current = currentControllerStack.current.filter(c => c !== controller);
            });
        }
        return <button onClick={handleClick}>送信</button>
    }
    ReactDOM.createRoot(document.getElementById('app')).render(<App/>);

 これで最新の操作による通信結果のみを返すことができ、非同期通信の結果をいい感じに反映しやすくなります

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG