loading...
Cover image for React And Web Workers

React And Web Workers

daviddalbusco profile image David Dal Busco Originally published at Medium ・4 min read

I share one trick a day until (probably not) the end of the COVID-19 quarantine in Switzerland, April 19th 2020. Twelve days left until hopefully better days.


I recently published Tie Tracker, a simple, open source and free time tracking app ⏱.

Among its features, the full offline mode was particularly interesting to develop. From an architectural point of view, I had to find a solution to compute, for statistical or exportation purposes, the many entries the users are potentially able to record without blocking the user interface.

That’s why I had the idea to solve my problem with the help of the Web Workers API.

The app is developed with Ionic + React, therefore let me share with you my recipe 😉.


Simulate A Blocked User Interface

Before trying Web Workers out, let’s first try to develop a small application which contains an action which actually block the user interface.

In the following component, we are handling two states, two counters. One of these is incremented on each button click while the other call a function incApple() which loops for a while and therefore block the user interaction.

import {
    IonContent,
    IonPage,
    IonLabel,
    IonButton
} from '@ionic/react';
import React, {useState} from 'react';
import {RouteComponentProps} from 'react-router';

import './Page.css';

const Page: React.FC<RouteComponentProps<{ name: string; }>> = ({match}) => {

    const [countTomato, setCountTomato] = useState<number>(0);
    const [countApple, setCountApple] = useState<number>(0);

    function incApple() {
        const start = Date.now();
        while (Date.now() < start + 5000) {
        }
        setCountApple(countApple + 1);
    }

    return (
        <IonPage>
            <IonContent className="ion-padding">
                <IonLabel>Tomato: {countTomato} | Apple: {countApple}</IonLabel>

                <div className="ion-padding-top">
                    <IonButton 
                     onClick={() => setCountTomato(countTomato + 1)}
                     color="primary">Tomato</IonButton>

                    <IonButton 
                     onClick={() => incApple()} 
                     color="secondary">Apple</IonButton>
                </div>
            </IonContent>
        </IonPage>
    );
};

export default Page;

As you can notice in the following animated Gif, as soon as I start the “Apple counter”, the user interaction on the “Tomato counter” have no effects anymore, do not trigger any new component rendering, as the function is currently blocking the JavaScript thread.


Defer Work With Web Workers

Having the above example in mind, let’s try out Web Workers in order to defer our “Apple counter” function.


Web Workers

To easiest way to add a Web Worker to your application is to ship it as an asset. In the case of my Ionic React application, these find place in the directory public , that’s we create a new file ./public/workers/apple.js .

Before explaining the flow of the following code, two things are important to notice:

  1. The application and the Web Workers are two separate things. They don’t share states, they don’t share libraries, they are separate and can communicate between them through messages only.

  2. Web Workers do not have access to the GUI, to the document, to the window.

If you are familiar with Firebase, you can kind of understand, to some extent, the Web Worker as your own private, not Cloud, but local functions.

The entry point of our web worker is onmessage which is basically a listener to call triggered from our application. In the function we are registering, we are checking if a corresponding msg is provided, this let us use a web worker for many purposes, and are also amending the current counter value before running the same function incApple() as before. Finally, instead of updating the state directly, we are returning the value to the application through a postMessage .

self.onmessage = async ($event) => {
    if ($event && $event.data && $event.data.msg === 'incApple') {
        const newCounter = incApple($event.data.countApple);
        self.postMessage(newCounter);
    }
};

function incApple(countApple) {
    const start = Date.now();
    while (Date.now() < start + 5000) {
    }
    return countApple + 1;
}

Interacting With The Web Workers

To interact with the web worker, we first need to add a reference point to our component.

const appleWorker: Worker = new Worker('./workers/apple.js');

Because we are communicating with the use of messages, we should then register a listener which would take care of updating the counter state when the web worker emits a result.

useEffect(() => {
    appleWorker.onmessage = ($event: MessageEvent) => {
        if ($event && $event.data) {
            setCountApple($event.data);
        }
    };
}, [appleWorker]);

Finally we update our function incApple() to call the web worker.

function incApple() {
    appleWorker
         .postMessage({msg: 'incApple', countApple: countApple});
}

Tada, that’s it 🎉. You should now be able to interact with the GUI even if the “blocker code is running”. As you can notice in the following animated Gif, I am still able to increment my tomato counter even if the blocking loops is performed by the web worker.

The component altogether in case you would need it:

import {
    IonContent,
    IonPage,
    IonLabel,
    IonButton
} from '@ionic/react';
import React, {useEffect, useState} from 'react';
import {RouteComponentProps} from 'react-router';

import './Page.css';

const Page: React.FC<RouteComponentProps<{ name: string; }>> = ({match}) => {

    const [countTomato, setCountTomato] = useState<number>(0);
    const [countApple, setCountApple] = useState<number>(0);

    const appleWorker: Worker = new Worker('./workers/apple.js');

    useEffect(() => {
        appleWorker.onmessage = ($event: MessageEvent) => {
            if ($event && $event.data) {
                setCountApple($event.data);
            }
        };
    }, [appleWorker]);

    function incApple() {
        appleWorker
            .postMessage({msg: 'incApple', countApple: countApple});
    }

    return (
        <IonPage>
            <IonContent className="ion-padding">
                <IonLabel>Tomato: {countTomato} | Apple: {countApple}</IonLabel>

                <div className="ion-padding-top">
                    <IonButton 
                     onClick={() => setCountTomato(countTomato + 1)}
                     color="primary">Tomato</IonButton>

                    <IonButton 
                     onClick={() => incApple()} 
                     color="secondary">Apple</IonButton>
                </div>
            </IonContent>
        </IonPage>
    );
};

export default Page;

Summary

Web Workers is really an interesting concept. Tie Tracker let me experiment them and I am definitely going to use them again in future projects. Its code is open source and available on GitHub. If you have any feedback and even better, are interested to contribute, send me your best Pull Requests, that would be awesome 😎.

Stay home, stay safe!

David

Cover photo by Tobias Tullius on Unsplash

Posted on by:

daviddalbusco profile

David Dal Busco

@daviddalbusco

Creator of DeckDeckGo | Organizer of the Ionic Zürich Meetup

Discussion

pic
Editor guide
 

Thanks for the article David! I know you have an Angular brackground too, therefore RxJS... Are you aware of any benefits one can get from using web workers that are not present when using Observables or, why not, Promises?

 

I think that Web Worker addresses another need that Promises/Observables.

Promises are asynchronous operations as they give the promise of a result not yet available (MDN docs).

Observables are asynchronous too.

But, JavaScript is single threaded, therefore yes these operations are asynchron but when their related code is executed ("when the code inside the promise or observable runs") then it uses the single thread and therefore block any other evaluation.

Web Workers are asynchronous too but to the contrary run in separate threads, therefore, when their code is executed, it doesn't block the main single JavaScript thread of the application.

Does that make sense?

P.S.: Here also the "blocking example" of my blog post in which I added a promise. Even it does contains such promise, it would still block the interaction as displayed in the related animated gif.

import {
    IonContent,
    IonPage,
    IonLabel,
    IonButton
} from '@ionic/react';
import React, {useState} from 'react';
import {RouteComponentProps} from 'react-router';

import './Page.css';

const Page: React.FC<RouteComponentProps<{ name: string; }>> = ({match}) => {

    const [countTomato, setCountTomato] = useState<number>(0);
    const [countApple, setCountApple] = useState<number>(0);

    async function incApple() {
        await deferIncApple();
    }

    function deferIncApple(): Promise<void> {
        return new Promise<void>((resolve) => {
            const start = Date.now();
            while (Date.now() < start + 5000) {
            }
            setCountApple(countApple + 1);

            resolve();
        });
    }

    return (
        <IonPage>
            <IonContent className="ion-padding">
                <IonLabel>Tomato: {countTomato} | Apple: {countApple}</IonLabel>

                <div className="ion-padding-top">
                    <IonButton
                        onClick={() => setCountTomato(countTomato + 1)}
                        color="primary">Tomato</IonButton>

                    <IonButton
                        onClick={() => incApple()}
                        color="secondary">Apple</IonButton>
                </div>
            </IonContent>
        </IonPage>
    );
};

export default Page;