Setup workers with Create React App and Typescript

February 14, 2020

Introduction

I was struggeling a lot setting up this, and to be honest, It’s a pain.
So here comes a brief summary of resources and steps to setup workers.

Necessary Packages and Tools

Worker Loader

It’s easy to setup a worker in Vanilla-JS and Create React App is prepared for this but if you want use your library code you will struggle on this issue. What you need is to install a worker-loader package, in my case I have used worker-loader.

$ yarn add worker-loader

To configure the loader you need to customize the webpack-configuration with the help of React App Rewired.

React App Rewired

You have to add a rule to the webpack.config.js!
For this reason you have to work with react-app-rewired or eject your configuration to change it.

$ yarn add react-app-rewired

To override the webpack-config you need to add a file ‘config-overrides.js’ to your root-folder of the project.

$ cat config-overrides.js
module.exports = function override(config, env) {
    config.output.globalObject = 'this';
    config.module.rules.push({
        test: /\.worker\.js$/,
        use: [
            {loader: 'worker-loader'},
            {loader: 'babel-loader'}
        ]
    });
    return config;
};

Also you should adapt your package.json’s script-scetion:

....

  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "lint": "tslint --project . --fix",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  },

....

Implementing the worker

The worker-code:

// /src/scenes/t03/components/content/observation/observation.worker.ts
// the filename must end on: .worker.ts or .worker.js 

import calculateStatistics from '../../../../../services/statistics/calculateStatistics';
import {IObservationWorkerInput, IObservationWorkerResult} from './observation.worker.type';

export const CALCULATE_STATISTICS_INPUT = 'CALCULATE_STATISTICS_INPUT';
export const CALCULATE_STATISTICS_RESULT = 'CALCULATE_STATISTICS_RESULT';

const ctx: Worker = self as any;

// Respond to message from parent thread
ctx.addEventListener('message', (e) => {
    if (!e) {
        return;
    }

    const message: IObservationWorkerInput = e.data;
    const {type, data} = message;

    if (type === CALCULATE_STATISTICS_INPUT) {
        const statistics = calculateStatistics(data.data, data.exclude);
        postMessage({
            type: CALCULATE_STATISTICS_RESULT,
            data: statistics
        });
    }
});

The Type definitions:

import {IHobData, IStatistics} from './statistics';

export interface IObservationWorkerInput {
    type: string;
    data: IObservationInputData;
}

export interface IObservationWorkerResult {
    type: string;
    data: IStatistics;
}

export interface IObservationInputData {
    data: IHobData;
    exclude: string[];
}

The React-Component:

// /src/scenes/t03/components/content/observation/observation.ts

import React, {useEffect, useState} from 'react';
import {CALCULATE_STATISTICS_INPUT, CALCULATE_STATISTICS_RESULT} from './observation.worker';

....
....

let w: Worker | undefined;

// Wrapped function to avoid JEST-errors in test-environment
const loadWorker = () => {
    let worker;
    try {
        // tslint:disable-next-line:no-var-requires
        worker = require('worker-loader!./observation.worker');
    } catch (e) {
        if (process.env.NODE_ENV !== 'test') {
            throw e;
        }
    }

    return new worker() as Worker;
};

const observation = () => {

  const [isCalculating, setIsCalculating] = useState<boolean>(false);
  const [statistics, setStatistics] = useState<IStatistics | null>(null);

  useEffect(() => {

    // loads the worker when component mounts
    w = loadWorker();
    w.addEventListener('message', handleMessage);

    // finishes on unmount
    return () => {
      if (w) {
        // @ts-ignore
        w.removeEventListener('message');
        w.terminate();
      }
    };
  }, []);

  const calculate = () => {
    if (w) {
        setIsCalculating(true);
        w.postMessage({
            type: CALCULATE_STATISTICS_INPUT,
            data: {
                data: hobData,
                exclude: excludedWells
            }
        });
    }
  }

  const handleMessage = (m: any) => {
      const message: IObservationWorkerResult = m.data;
      if (message && message.type === CALCULATE_STATISTICS_RESULT) {
          setIsCalculating(false);
          setStatistics(message.data);
      }
  };

  return (
    // Rendering here...
  )

  ....
}

DONE!!