12 open source browser tools and how I've built them
Tags: react, pwa, javascript, webpack
Published: 2020-12-21 08:48:31

I'm happy to announce that I've finished development of [Omatsuri](https://omatsuri.app/) – [open source](https://github.com/rtivital/omatsuri) React PWA that includes 12 Frontend focused tools. In this post I'll share some insights on how these tools were built.

![Omatsuri landing](https://dev-to-uploads.s3.amazonaws.com/i/n8ihfxr0zuxkv6my4733.png)

## The tools

- CSS Triangle Generator
- Gradient Generator + Gradient Gallery
- CSS Cursors list
- Color Shades Generator
- Curved Page Dividers Generator
- SVG compressor
- SVG to JSX converter
- Base64 encoder
- Realistic Fake Data Generator
- HTML/CSS Symbols Collection
- Lorem/Samuel/Poke Ipsum Generator
- JavaScript Events Keycodes

## Technical details

My main purpose was to make Omatsuri a browser only application. This approach allows to reduce costs for server hosting that does heavy jobs, like SVG compression, Prettier transformations, encodings and other heavy things. This also means that application will always be fully accessible offline without any limitations.

### Service worker and offline support

Since Omatsuri is a browser only application the only thing that we need from service worker is to cache assets and provide app shell. [Offline plugin](https://github.com/NekR/offline-plugin) does exactly that, the only thing we need to do – add it to the production build in [webpack config](https://github.com/rtivital/omatsuri/blob/master/webpack.config.js#L192):

```js
new OfflinePlugin({ autoUpdate: true, appShell: '/', excludes: ['404.html', 'CNAME'] }),
```

Now we are ready to [listen to service worker ready state](https://github.com/rtivital/omatsuri/blob/master/src/components/Navbar/Navbar.jsx#L43-L47) and propose user to install PWA when it's done loading:
```js
useEffect(() => {
  navigator.serviceWorker.ready
    .then(() => setOffline({ ready: true, error: false }))
    .catch(() => setOffline({ ready: false, error: true }));
}, []);
```

### Github Pages and React Router

Omatsuri is hosted on Github Pages – it's free and does fine job of serving static assets. There is only one problem – it does not work well with browser history and as I was building a SPA I wanted to fully control routing with React Router. 

For example, gh-pages will return index.html for / request, but there is no way to force it to return the same index.html for /triangle-generator route. 

The workaround here is to create separate 404.html with the same content as in index.html – gh-pages will send it for each request that cannot be found in static files and  service worker will do the rest of the job. This is not perfect as gh-pages will return 404 status, but at least it works fine.

Another issue with gh-pages – small cache TTL (10 minutes), it lowers Lighthouse score, but is not critical since we have service worker.

![Omatsuri lighthouse score](https://dev-to-uploads.s3.amazonaws.com/i/hvh3kig3d53kfe52usb6.png)


### SVG compression

There is actually only one good library for SVG compression ([SVGO](https://github.com/svg/svgo)) written in JavaScript. And it does not have browser support, only Node.js. I found it very strange as compression is based entirely on string parsing and does not include any node specific logic. 

So my first task was to migrate SVGO to browser. It was pretty easy, since all core logic did not require any modifications. And now you can use [svgo-browser](https://github.com/rtivital/svgo-browser) library in your projects if you ever need SVG compression in browser.


### Web workers

Some task are very heavy and can block your browser for several seconds. To fix this, we can put them in separate thread using web workers and they will run in background without blocking the main thread. 

I was surprised how easy it is to work with web workers in webpack. All you need is [worker-loader](https://github.com/webpack-contrib/worker-loader) that will handle all worker bundling for you.

Here is an [example](https://github.com/rtivital/omatsuri/blob/master/src/workers/svg-to-jsx.worker.js) of web worker usage for transforming svg to jsx with prettier and svg compression:
```js
// svg-to-jsx.worker.js
  
import prettier from 'prettier/standalone';
import prettierBabel from 'prettier/parser-babel';
import svg2jsx from 'svg-to-jsx';
import optimize from 'svgo-browser/lib/optimize';

function generateComponent(svg) {
  return `import React from 'react';\n\nexport default function SvgComponent() { return ${svg} }`;
}

onmessage = (event) => {
  const { payload } = event.data;

  optimize(event.data.content)
    .then((content) => svg2jsx(content))
    .then((svg) =>
      prettier.format(generateComponent(svg), { parser: 'babel', plugins: [prettierBabel] })
    )
    .then((code) => postMessage({ error: null, payload, code }))
    .catch((error) => postMessage({ error, payload, content: null }));
};
```

```js
// react component

import React, { useState, useLayoutEffect } from 'react';
import Svg2jsxWorker from '../../workers/svg-to-jsx.worker';

const svg2jsx = new Svg2jsxWorker();

export default function SvgToJsx() {
  const [result, setResult] = useState({ loading: false, error: null, content: null });

  const handleMessage = (event) => {
    setResult({ loading: false, error: event.data.error, content: event.data.code });
  };

  const postMessage = (text) => svg2jsx.postMessage({ content: text });

  useLayoutEffect(() => {
    svg2jsx.addEventListener('message', handleMessage);
    return () => svg2jsx.removeEventListener('message', handleMessage);
  }, []);

  return (/* ... */);
}
```

### Dark theme support

By default Omatsuri uses system theme, to listen to those changes, I've created [react hook](https://github.com/rtivital/xooks/blob/master/src/use-color-scheme/use-color-scheme.ts) that returns current browser theme:

```ts
import { useState, useEffect } from 'react';

const media = window.matchMedia('(prefers-color-scheme: dark)');

export default function useColorScheme() {
  const [scheme, setScheme] = useState<'dark' | 'light'>(media.matches ? 'dark' : 'light');
  const handleSchemeChange = (query: { matches: boolean }) =>
    setScheme(query.matches ? 'dark' : 'light');

  useEffect(() => {
    media.addEventListener('change', handleSchemeChange);
    return () => media.removeEventListener('change', handleSchemeChange);
  }, []);

  return scheme;
}
```

It's not enough though, since I wanted to give an option  to change theme. To achieve that I've created [ThemeProvider component](https://github.com/rtivital/omatsuri/blob/master/src/ThemeProvider.jsx) that wraps entire application and provides theme value via react context. To get theme in any component all I need is to call useTheme hook:

```js
const [theme] = useTheme();
```

## Conclusions

Omatsuri was my first PWA and I really enjoyed the process – existing tools make it super easy to transform your regular React SPA to PWA and utilize complex things like web workers.

During the development apart from Omatsuri itself I've created two additional npm libraries:

- [xooks](https://github.com/rtivital/xooks) – React hooks library that includes all hooks that I've used during Omatsuri development (localstorage manipulations, system theme detection, clipboard utils and six others).
- [svgo-browser](https://github.com/rtivital/svgo-browser) – svgo fork with better Node.js API and browser support.

## Support Omatsuri

If you like Omatsuri please give it a star on Github – https://github.com/rtivital/omatsuri and install it as PWA – this will assure that you have all 12 tools even when you are offline.

Thanks for your support!