Search code examples
reactjsecmascript-6web-worker

How to call a shared method from a react app and a web worker


How can I reference a utility class in both a React app and a web worker?

Here's a bare-bones create-react-app that demonstrates my core issue. The web worker code itself works fine, it just doesn't have access to the Util class.

App.js

import React, { Component } from 'react';
import './App.css';
import Util from './Util.js'
import worker from './Util.worker.js';
import WebWorker from './WebWorker.js';

export default class App extends Component {
    constructor(props) {
        super(props);
        this.util = new Util();
        this.state = { something: "" };
    }
    componentDidMount() {
        this.worker = new WebWorker(worker);
        this.worker.addEventListener('message', e => {
            this.setState({ something: e.data })
        });
    }
    render() {
        return (
            <div className="App">
                <p onClick={e => this.randomHandler()}>{this.util.DoSomething("hello")}</p>
                <p>{this.state.something}</p>
            </div>
        );
    }
    randomHandler() {
        console.log("err")
        this.worker.postMessage(["cool"]);
    }
}

Util.js

export default class Util {
    DoSomething(x) {
        return this.AnotherMethod(x) + "!";
    }
    AnotherMethod(x) {
        return x;
    }
}

WebWorker.js

export default class WebWorker {
    constructor(worker) {
        const code = worker.toString();
        const blob = new Blob(['(' + code + ')()']);
        return new Worker(URL.createObjectURL(blob));
    }
}

Util.worker.js

export default () => {
    self.addEventListener('message', function(e) { // eslint-disable-line no-restricted-globals
        postMessage("How do I call Util.DoSomething() here?");
    }, false);
}

Here's what I want to be able to do.

var util = new Util();
postMessage(util.DoSomething(e.data));

Typically a web worker is instantiated on a file path, but that doesn't play well with React due to the way the app is compiled. That's why several sources recommended serializing the method and dynamically creating the worker from that (which is what I'm doing).

I've tried passing both serialized instances of the class and the class itself to the worker, but that didn't work out (and it feels hackish).

I've also tried importing the Util class in the web worker, but I just get reference errors because the WebWorker class doesn't process the import.

Could someone with experience in this area point out what I'm doing wrong?


Solution

  • Unfortunately the answer was to eject the configs from Create React App. Once this pull request is merged we shouldn't need to do that.

    Run these commands. Note that the eject command is irreversible.

    npm run eject
    npm install thread-loader
    npm install worker-loader
    

    Add this to the webpack.config (both dev and prod) in the module>rules>oneOf section.

    {
      test: /\.worker\.(js|jsx|mjs)$/,
      include: paths.appSrc,
      use: [
        require.resolve('worker-loader'),
        // This loader parallelizes code compilation, it is optional but
        // improves compile time on larger projects
        require.resolve('thread-loader'),
        {
          loader: require.resolve('babel-loader'),
          options: {
            // @remove-on-eject-begin
            babelrc: false,
            presets: [require.resolve('babel-preset-react-app')],
            // @remove-on-eject-end
            // This is a feature of `babel-loader` for webpack (not Babel itself).
            // It enables caching results in ./node_modules/.cache/babel-loader/
            // directory for faster rebuilds.
            cacheDirectory: true,
            highlightCode: true,
          },
        },
      ],
    },
    

    Then Util.worker.js becomes this:

    import Util from './Util.js'
    
    self.addEventListener('message', function(e) {
        var util = new Util();
        self.postMessage(util.DoSomething(e.data)); 
    }, false);
    

    And App.js becomes this:

    import React, { Component } from 'react';
    import './App.css';
    import Util from './Util.js'
    import Worker from './Util.worker.js';
    
    export default class App extends Component {
        constructor(props) {
            super(props);
            this.util = new Util();
            this.state = { something: "" };
        }
        componentDidMount() {
            this.worker = new Worker();
            this.worker.onmessage = e => {
                console.warn(e.data);
                this.setState({ something: e.data });
            }
        }
        render() {
            return (
                <div className="App">
                    <p onClick={e => this.randomHandler()}>{this.util.DoSomething("hello")}</p>
                    <p>{this.state.something}</p>
                </div>
            );
        }
        randomHandler() {
            console.log("err")
            this.worker.postMessage("cool");
        }
    }