3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React で WebWorkerを使う方法

Posted at

React + WebWorker + ときどきwebpack

React + WebWorker を実現するために試行錯誤した記録とたどり着いた解決策をここに記します。

環境

webpack5 + React 17 (React 18 でも同じことですが)

React は値を保持しない

まず知っておくこととして関数コンポーネントは純粋関数でなくてはなりません。

そのため関数は再レンダリングにまたがって値を保持する方法として提供しているのが

useState, useRef, useMemo, useCallback等であり、

関数コンポーネント内で宣言された JavaScript 変数は再レンダリングをまたがって保持しません。

毎度レンダリング時に関数コンポーネントは再実行され、以前の変数は消え、今回のレンダリングで同じ変数は初期値を与えられます。

そのため worker を使うためには

どうにかして webworker のインスタンスを再レンダリング時にまたがって保持する必要があります。

よく見かける提案

ネットを検索するとわりかし次の記事のような提案をよく見かけます。

関数コンポーネントで webworker を使うためには、useMemo()を使うことをよく提案されています。

// 関数コンポーネント内にて
const worker: Worker = useMemo(
    () =>
        new Worker(
            new URL('/src/worker/Counter.worker.ts', import.meta.url)
        ),
    []
);

useMemo()の触れ込み通りならば、

依存関係ぬきでuseMemo()で worker インスタンスを生成すれば、

マウント時にこれを実行すれば関数コンポーネントがアンマウントされない限り worker インスタンスは毎レンダリングをまたがって保持されるはずです。

検証してみましょう。

検証

検証コード

worker に負荷の高い計算を任せて、

その計算が済んだら返してもらって結果を表示するプログラムを用意しました。

とにかく React で worker が使えるのかどうかだけ確かめるだけです。

ディレクトリ構成:

public/
    index.html
src/
    index.tsx
    App.tsx
    components/
        FunctionalCase.tsx
    worker/
        Coutner.worker.ts
tsconfig.json
webpack.config.js
// FunctionalCase.tsx
import React, { useEffect, useMemo, useState } from 'react';
import type { iRequest, iResponse } from './worker/Counter.worker';

const FunctionalCase = () => {
    const [counter, setCounter] = useState<number>(0);

    const worker: Worker = useMemo(
        () =>
            new Worker(
                new URL('/src/worker/Counter.worker.ts', import.meta.url)
            ),
        []
    );

    useEffect(() => {
        console.log('did mount');

        if (window.Worker) {
            console.log('set message listener');

            worker.addEventListener('message', handleWorkerMessage);
        }

        return () => {
            if (window.Worker) {
                console.log('termnate worker');

                worker.removeEventListener('message', handleWorkerMessage);
                worker.terminate();
            }
        };
    }, []);

    const handleWorkerMessage = (e: MessageEvent<iResponse>) => {
        const { count } = e.data;
        console.log(`Got message from worker: ${count}`);
        setCounter(count);
    };

    const handleClick = () => {
        console.log('click');

        worker.postMessage({
            count: counter,
            order: 'calculate',
        } as iRequest);
    };

    return (
        <div className="functional-case">
            <h2>In case functional component with Worker</h2>
            <button onClick={handleClick}>count up</button>
            <h2>{counter}</h2>
        </div>
    );
};

export default FunctionalCase;
// Counter.worker.ts

export interface iRequest {
    order: 'calculate';
    count: number;
}

export interface iResponse {
    count: number;
}

const expensiveCalculator = (val: number) => {
    let result = val;
    for (let i = 0; i < 10000; i++) {
        result += 1;
    }
    return result;
};

self.addEventListener('message', (e: MessageEvent<iRequest>) => {
    const { order, count } = e.data;
    // To ignore other posted message.
    if (order !== 'calculate') return;

    const result = expensiveCalculator(count);
    self.postMessage({
        count: result,
    });
});

以下のような処理の流れが期待されます。

  • <button onClick={handleClick}>count up</button>のボタンをクリック
  • Counter.workerへメッセージを送信し
  • Counter.workerは 1 万ループする無駄計算を実行し 1 万増えた値をFunctionaleCaseへ返す
  • FunctionaleCasecoutnerが更新される

実行結果

ではアプリケーションを実行して、counter のボタンを押してみましょう。

$ npm run start
# ....
did mount
set message listener
termnate worker
# StrictModeによる再実行
did mount
set message listener
[Counter.worker] running...
[Counter.worker] running...
# ボタンをクリックしてworkerへメッセージを送信したが...
click

# ...なにも起こらない

clickの表示以降何も変化しません。

なぜでしょう?

原因を追究します。

メッセージを取得したらコンソールに表示されるようにしているので、メッセージ自体が届いていない可能性があります。

message がやり取りできない原因

origin が違うから?

origin は同じでした。

アプリケーションはローカルで実行しており、

メイン環境、ワーカー環境どちらもhttp://localhost:8080であることを確認済です。

React.StrictModeで二度実行されていることが関係しているから?

結論を言うとこれが原因です。

事実、StrictModeを無くすと期待通りに動きます。

そのためStrictModeが何かしら関係していることが考えられます。

worker インスタンスを維持する手段として useMemo()が使えない原因

原因ははっきりと特定できていません。

しかし以下のようではないかなと思っています。

原因の所在はReact.StrictModeによりコンポーネントが二度実行されている点にあることははっきりしています。

useMemoStrictModeにより 2 度実行され、そのuseMemo呼出のうちのいずれかは破棄されます。
公式は、useMemoの使い方としては、計算関数は純粋関数を使っているはずだから、何度実行しても同じ値が返されるはずだからどちらを破棄しても問題ないよねというスタンスです。

なので、可能性としては

React は初期生成ワーカーを残すつもりで再実行で生成されるワーカーを破棄したが、

実はクリーンアップ関数で初期生成ワーカーがterminate()されていた...

というシナリオはあり得ます。

React が破棄する方の値が常に最初に生成した方であった場合、

useMemo()はクリーンアップ関数で処理が必要な値を扱ってはならないということになり、そうであった場合関数コンポーネントは worker を terminate()出来ないことが明らかになります。

実際StrcitModeなしで実行すれば発生しない問題なので worker が二度実行されている点、useMemo()を使っている点から推測するとなくはないと思っています。

解決策: useRefを使う

そもそも公式でもないネットに載っているような方法を使っているのが原因では?というのが根本原因なので、

基本を思い出し、worker は React の理の外である、つまりuseRefを使うべきと気づくべきでした。

検証

import React, { useEffect, useMemo, useState, useRef } from 'react';
import type { iRequest, iResponse } from './worker/Counter.worker';

const FunctionalCase = () => {
    const [counter, setCounter] = useState<number>(0);
    const refWorker = useRef<Worker>();

    // const worker: Worker = useMemo(
    //     () =>
    //         new Worker(
    //             new URL('/src/worker/Counter.worker.ts', import.meta.url)
    //         ),
    //     []
    // );

    useEffect(() => {
        console.log('did mount');

        // if (window.Worker) {
        //     console.log('set message listener');

        //     worker.addEventListener('message', handleWorkerMessage);
        // }

        if (window.Worker && refWorker.current === undefined) {
            console.log('Generate worker and set message listener');

            refWorker.current = new Worker(
                new URL('/src/worker/Counter.worker.ts', import.meta.url)
            );
            refWorker.current.addEventListener('message', handleWorkerMessage);
        }

        return () => {
            if (window.Worker && refWorker.current) {
                console.log('terminate worker');

                // worker.removeEventListener('message', handleWorkerMessage);
                // worker.terminate();

                refWorker.current.removeEventListener(
                    'message',
                    handleWorkerMessage
                );
                refWorker.current.terminate();
                refWorker.current = undefined;
            }
        };
    }, []);

    const handleWorkerMessage = (e: MessageEvent<iResponse>) => {
        const { count } = e.data;
        console.log(`Got message from worker: ${count}`);
        setCounter(count);
    };

    const handleClick = () => {
        console.log('click');

        if (refWorker.current === undefined) return;

        refWorker.current.postMessage({
            count: counter,
            order: 'calculate',
        } as iRequest);
    };

    return (
        <div className="functional-case">
            <h2>In case functional component with Worker</h2>
            <button onClick={handleClick}>count up</button>
            <h2>{counter}</h2>
        </div>
    );
};

export default FunctionalCase;

結果:

# workerコンテキストが生成された
[Counter.worker] running...
# 初期マウント
did mount
Generate worker and set message listener
terminate worker
# StrictModeによる再実行
did mount
Generate worker and set message listener
[Counter.worker] running...

# リクエストボタンをクリックしてみたら...
click
[Counter.worker] got request
[Counter.worker] send result
Got message from worker: 10000

click
[Counter.worker] got request
[Counter.worker] send result
Got message from worker: 20000

# このとおりworkerと通信出来た。

上記のコードのとおり、

terminate()した後にref.currentへ undefined を渡さないとなりません。

これをしなかった場合、useEffectの再実行時にrefWorker.currentはまだオブジェクトを保持しています。

オブジェクトが残っているのならば worker と通信できるのでは?と思っても通信はできません。

試してみたのですが worker のコンテキストは多分消えています。

なのでterminate()の役割は、worker コンテキストの始末(メモリの解放とか?)であって、

当然ですが worker インスタンスである値が undefined やら null になるわけではありません。

ここら辺は先のuseMemo()が使えなかった原因に結びつくかもしれません。

class コンポーネントはまだ現役

先の通りの方法でほぼ解決かと思いますが、class コンポーネントを使うという手段もあります。

つまり、class の field として worker インスタンスを保持するという方法です。

検証

import React from 'react';
import type { iResponse, iRequest } from './worker/Counter.worker';

interface iProps {}
interface iState {
    counter: number;
}

class ClassCase extends React.Component<iProps, iState> {
    state = {
        counter: 0,
    };
    _worker: Worker | undefined;

    constructor(props: iProps) {
        super(props);
        this.handleMessage = this.handleMessage.bind(this);
        this.handleClick = this.handleClick.bind(this);
    }

    componentDidMount() {
        console.log('did mount');
        if (window.Worker && this._worker === undefined) {
            console.log('generate and setup worker');

            this._worker = new Worker(
                new URL('/src/worker/Counter.worker.ts', import.meta.url)
            );
            this._worker.addEventListener('message', this.handleMessage);
        }
    }

    componentDidUpdate() {
        console.log('did update');
    }

    componentWillUnmount(): void {
        if (window.Worker && this._worker !== undefined) {
            console.log('terminate worker');

            this._worker.removeEventListener('message', this.handleMessage);
            this._worker.terminate();
            this._worker = undefined;
        }
    }

    handleMessage(e: MessageEvent<iResponse>) {
        const { count } = e.data;
        console.log(`Got message from worker: ${count}`);
        this.setState({ counter: count });
    }

    handleClick() {
        console.log('click');

        if (this._worker === undefined) return;

        this._worker.postMessage({
            count: this.state.counter,
            order: 'calculate',
        } as iRequest);
    }

    render() {
        return (
            <div className="functional-case">
                <h2>In case functional component with Worker</h2>
                <button onClick={this.handleClick}>count up</button>
                <h2>{this.state.counter}</h2>
            </div>
        );
    }
}

export default ClassCase;

結果:

Counter.worker.ts:18 [Counter.worker] running...
03:26:46.701 ClassCase.tsx:20 did mount
03:26:46.704 ClassCase.tsx:22 generate and setup worker
03:26:46.707 ClassCase.tsx:37 terminate worker
03:26:46.708 ClassCase.tsx:20 did mount
03:26:46.709 ClassCase.tsx:22 generate and setup worker
03:26:50.209 Counter.worker.ts:18 [Counter.worker] running...

# リクエストボタンを押してみたら
03:26:55.065 ClassCase.tsx:52 click
03:26:55.066 Counter.worker.ts:25 [Counter.worker] got request
03:26:55.067 Counter.worker.ts:29 [Counter.worker] send result
03:26:55.071 ClassCase.tsx:47 Got message from worker: 10000
03:26:55.082 ClassCase.tsx:32 did update
03:26:59.521 ClassCase.tsx:52 click
03:26:59.522 Counter.worker.ts:25 [Counter.worker] got request
03:26:59.522 Counter.worker.ts:29 [Counter.worker] send result
03:26:59.525 ClassCase.tsx:47 Got message from worker: 20000
03:26:59.532 ClassCase.tsx:32 did update

# 先の例と同じ結果になった

React+Webpack での worker の扱い方

設定

webpack 5 でのお話です。

基本公式の通りでいいと思います。

リンクのページでは webpack.config.js の具体的な設定は載っていませんが、以下の通りで問題なく動いています。

const path = require('path');
const HtmlWebPackPlugin = require('html-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

const isDevelopment = process.env.NODE_ENV !== 'production';

module.exports = {
    mode: 'development',
    entry: {
        index: './src/index.tsx',
        'your.worker': './src/worker/your.worker.ts',
        'another.worker': './src/worker/another.worker.ts',
    },
    resolve: {
        extensions: ['.js', '.jsx', '.tsx', '.ts'],
    },
    output: {
        globalObject: 'self',
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'dist'),
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx|tsx|ts)$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: require.resolve('babel-loader'),
                        options: {
                            presets: [
                                '@babel/preset-env',
                                '@babel/preset-typescript',
                                '@babel/preset-react',
                            ],
                            plugins: [
                                isDevelopment &&
                                    require.resolve('react-refresh/babel'),
                            ].filter(Boolean),
                        },
                    },
                ],
            }
        ],
    },
    plugins: [
        new HtmlWebPackPlugin({
            template: 'src/index.html',
        }),
        isDevelopment && new ReactRefreshWebpackPlugin(),
    ].filter(Boolean),
};

重要なポイントは

  • globalObject: 'self'
  • entryに worker ファイルを含める

targetプロパティがデフォルト(web)のままだけど問題なくworkerは生成されるみたいです。

webpack で webworker を扱ううえでの注意

バンドルする関係上予期せぬライブラリが依存関係に含まれる

webpack は worker ファイルのの依存関係をすべてひとつにバンドルします。

すると以下のようなエラーに遭遇することがあります。

browser.js:131 Uncaught (in promise) ReferenceError: window is not defined
    at eval (browser.js:131:1)
    at ./node_modules/monaco-editor/esm/vs/base/browser/browser.js (vendors-node_modules_monaco-editor_esm_vs_editor_editor_main_js-node_modules_idb-keyval_dist_-7d7b36.bundle.js:928:1)
    at options.factory (src_worker_fetchLibs_worker_ts.bundle.js:790:31)
    at __webpack_require__ (src_worker_fetchLibs_worker_ts.bundle.js:208:33)
    at fn (src_worker_fetchLibs_worker_ts.bundle.js:429:21)
    at eval (fontMeasurements.js:6:82)
    at ./node_modules/monaco-editor/esm/vs/editor/browser/config/fontMeasurements.js (vendors-node_modules_monaco-editor_esm_vs_editor_editor_main_js-node_modules_idb-keyval_dist_-7d7b36.bundle.js:3238:1)
    at options.factory (src_worker_fetchLibs_worker_ts.bundle.js:790:31)
    at __webpack_require__ (src_worker_fetchLibs_worker_ts.bundle.js:208:33)
    at fn (src_worker_fetchLibs_worker_ts.bundle.js:429:21)

ReferenceError: window is not definedというエラー。

本来、webworker のグローバル変数はDedicatedWorkerGlobalScopeというものになるはずで、

./node_modules/monaco-editor/esm/vs/base/browser/browser.jsという知らない奴がどういうわけかワーカー環境の中でwindowオブジェクトを参照しようとしているというエラーです。

しかしワーカーは./node_modules/monaco-editor/esm/vs/base/browser/browser.jsを一切 import していません。

そのため、どこに原因があるのかわからなくなることがありました。

そんなとき。

原因

こうなる原因は webpack が全ての依存関係をワーカーのために一つにバンドルするためでした。

つまり、バンドルした依存関係の中にwindowを必要とする依存関係が存在したのです。

ワーカーのコード:

// awesome.worker.ts
import { logger } from '../utils';

// ...以下略

一見一切monaco-editorのモジュールは import していない。

しかし実はutils/index.tsmonaco-editorを import しているモジュールを import していた。

それはsrc/utils/index.tsxである。

// utils/index.ts

// こいつ。
// `getModelByPath`はmonacoをインポートしている。
export { getModelByPath } from './getModelByPath';

// ...

// `awesome.worker`が取り込もうとしていた対象
export { logger } from "./logger";

src/utils/index.tsxはその内に./getModelByPathという monaco-editor を内に import しているモジュールを取りこんでいました。

webpack は、ワーカーがこのsrc/utils/index.tsxを import しているとみるや、src/utils/index.tsxが必要とする依存関係をすべてワーカーのバンドルに含めます。

そのためにワーカー単体では全く関係ないmonaco-editorモジュールを取り込むことになり、結果window前提のワーカーとなってしまったようです。

このように、worker を webpack で扱う際には依存関係に気をつけなくてはなりません。

webpack 詳しい人ならば当然の話かもしれませんが。

ということで、下記のようにワーカーにとっては余計な依存関係だらけのsrc/utils/index.tsが含まれないようにすれば解決です。

// awesome.worker.ts
- import { logger } from '../utils';
+ import { logger } from '../utils/logger';

ワーカーが変な依存関係をしていたら調べるといい場所

Chrome のデベロッパーツールのsourceより。

左側のペインのpage内容が...

top
    localhost:8080
    React DevelopperTool
    ....
your-worker-awesome-name....ts
fetchLibs_worker_ts_....ts

みたいに並んでいます。

調べたい worker がfetchLibs_worker_ts...だとしたらそれをクリックし


fetchLibs_worker_ts_....ts
    localhost:8080
    your-app-project-name
        node_modules
        src/worker

のようにひらきます。

ここの内容がそのワーカーfetchLIbs_worker_ts..の依存関係一覧です。

依存関係がおかしいと思ったらここの内容を調べておかしな依存関係が含まれていないかみてみると原因が見つかるかもしれません。

その他注意点: web event では React は更新されない

worker を使うにあたってpostMessageでやり取りする以上messageイベントでワーカーからのメッセージを受信することになります。

例えば以下のようにコールバック関数内で state の値を読み取っても正しい state の値を取得してくれません。

例:あるデータをリクエストして worker に取得(fetch)してもらい、workerから結果を受信したら state を更新するコンポーネント。

リクエスト段階ではそのリクエストしたモジュールの state プロパティをloadingに、

正常に受信出来たらloadedに更新する。

// A component that manages worker.
import React, {
    createContext,
    useState,
    useEffect,
    useRef,
    useContext,
} from 'react';

interface iDataState {
    dataId: string;
    state: 'loading' | 'loaded';
};

const ManageWorkercomponent = ({ children }) => {
    const [data, setData] = useState<iDataState[]>([]);
    const agent = useRef<Worker>();

    // Attach message event on mount.
    useEffect(() => {
        if (window.Worker && agent.current === undefined) {
            agent.current = new Worker(
                new URL('/src/worker/your.worker.ts', import.meta.url),
                { type: 'module' }
            );
            agent.current.addEventListener('message', handleWorkerMessage);
        }

        return () => {
            if (window.Worker && agent.current) {
                agent.current.removeEventListener(
                    'message',
                    handleWorkerMessage
                );
                agent.current.terminate();
                agent.current = undefined;
            }
        };
    }, []);

    // make sure data is updated correctly.
    useEffect(() => {
        console.log('did update');
        console.log(data);
    });

    // This callback function cannot access latest reactives...
    const handleWorkerMessage = (e: MessageEvent<yourMessageType>) => {
        const { dataId, dataMap } = e.data.payload;

        console.log(`Got response of ${dataId}`);
        // THIS `data` is always empty!!!!
        console.log(data);

        // ...
    };

    const requestFetchData = (dataId: string) => {

        const updatedDeps: iDataState[] = [
            ...data,
            {
                dataId,
                state: 'loading',
            },
        ];
        setData(updatedDeps);

        if (agent.current !== undefined) {
            agent.current!.postMessage({
                dataId
            });
        }
    };

    return (
        // ...
    );
};

簡単な流れ:

  • requestFetchData()を呼び出して取得したいデータをリクエストする
  • requestFetchData()はリクエストされたデータをひとまず data にstate: "loading"で登録する
  • worker にリクエストする
  • handleWorkerMessageが worker からのレスポンスを受信する。
  • dataの該当要素のプロパティstate:"loaded"に更新する。

このときhandleWorkerMessageから当然最新のdataが取得できることが期待されます。

しかしdataは空の配列を取得します。

なぜか?

理由は、React は web event で更新されないからと、messageイベントのコールバック関数は、worker にaddEventListener('message')をアタッチした時点の state の値しか読み取れなくなるからです。

つまり、

const [data, setData] = useState<iDataState[]>([]);

マウント時にイベントリスナを Worker へアタッチしたので、その時点では state dataの値は初期値の空配列です。

そしてイベントリスナをアタッチするとその時点の state dataにしかアクセスできなくなるため、いくら他でdataを更新しようとも常にアタッチ時のdataを取得することになるのです。

厄介なのが、setState 関数はアタッチ時の state と最新の state 両方に影響できるみたいで両方更新されます。

そのため、この場合であれば、別の場所でdataが更新されていてもmessageイベントのコールバックから setState すると空配列に対する更新が最新の state へ上書きされてしまうのです。

ということでイベントリスナをつけるとその時点の state だけを参照してしまうということがわかりました。

解決策

次の選択肢から選び取ることになります。

  1. useState を使うのをやめて useRef で管理する(参照を持たせる)。
  2. 毎レンダリングでaddEventListenerを付け替える。

1 の方法を採用するか否かは、値の更新が再レンダリングを起こすか起こさないかを天秤にかけて決めることになります。

useRefの利用ならば ref の参照を参照するだけになるので常にその最新の値をコールバック関数からでも追跡できます。ただしuseRefの値の更新は再レンダリングを起こしません。


const ManageWorkercomponent = ({ children }) => {
-   const [data, setData] = useState<iDataState[]>([]);
+   const data = useRef<iDataState[]>([]);
    // ...
}

2の方法は再レンダリングをやたら引き起こしたくない場合

// 先のコードに追加する。
    useEffect(() => {
        if (window.Worker && agent.current !== undefined) {
            agent.current.addEventListener('message', handleWorkerMessage);
        }
        return () => {
            if (window.Worker && agent.current !== undefined) {
                agent.current.removeEventListener(
                    'message',
                    handleWorkerMessage
                );
            }
        };
    }, [data]);

state dataの更新のたびにイベントリスナを再度アタッチします。こうすることで常に最新の値にアクセスできるようになります。

ということで React の理を web イベントのコールバックに含める場合は上記の工夫が必須となります。

これまでの話をまとめて webworker のカスタムフックを作る

汎用的なものではなく先の話に特化したフックです。

データがロード済かどうかを管理する state dataと、
データの中身を保存しておくdataMapを扱い、

worker とやり取りして2つの値を更新しその値を返します。

import React, { useState, useEffect, useRef } from "react"


export const useFetchDataWorker = (): [
    iData, Map<string, string> | undefined, (moduleName: string, version: string) => void
] => {
    const worker = useRef<Worker>();
    const [data, setData] = useState<iData[]>([]);
    const dataMap = useRef<Map<string, string>>(new Map<string, string>());

    useEffect(() => {
        if (window.Worker && agent.current === undefined) {
            agent.current = new Worker(
                new URL('/src/worker/your.worker.ts', import.meta.url),
                { type: 'module' }
            );
            agent.current.addEventListener('message', handleWorkerMessage);
        }

        return () => {
            if (window.Worker && agent.current) {
                agent.current.removeEventListener(
                    'message',
                    handleWorkerMessage
                );
                agent.current.terminate();
                agent.current = undefined;
            }
        };
    }, []);

    // `data`の毎度更新のたびにイベントリスナを付け替える
    useEffect(() => {
        if (window.Worker && agent.current === undefined) {
            agent.current.addEventListener('message', handleWorkerMessage);
        }
        return () => {
            if (window.Worker && agent.current) {
                agent.current.removeEventListener(
                    'message',
                    handleWorkerMessage
                );
            }
        };
    }, [data]);

    // 毎度イベントリスナを付け替えするのでそのままsetStateして問題ない
    const handleWorkerMessage = (event: MessageEvent<yourMessageType>) => {
        const { dataId, dataMap } = e.data;

        // Update dependencies, dependencyMap...
        // 配列stateの更新方法は公式を見てください...
    }

    const requestFetchLibrary = (dataId: string) => {
        setData([
            ...data, { dataId, state: "loading" }
        ]);
        if(agent.current !== undefined) {
            agent.current.postMessage({
                dataId
            });
        }
    }

    return [data, dataMap.current, requestFetchLibrary];
}

useRef で参照している値(dataMap)を返しており、更新されていない値を渡す可能性があるように見えますが、
dataMapの更新は常にdataの更新と同時に実施するため、再レンダリングをトリガーし両値の更新が担保されます。

まとめ

  • 関数コンポーネントで worker インスタンスはuseRefを使って保持すること。
  • もしくは class コンポーネントの field として保持すること
  • webpack でバンドルされるライブラリのグローバルスコープがDedicatedWorkerGlobalScope以外にならないか確認すること
  • Reactive な値を web イベントハンドラの中で使う場合、例えばuseStateの代わりにuseRefを使うか、イベントリスナの付け替えが必須である

参考

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?