DEV Community

Vitaly Rtishchev
Vitaly Rtishchev

Posted on

12 open source browser tools and how I've built them

I'm happy to announce that I've finished development of Omatsuriopen source React PWA that includes 12 Frontend focused tools. In this post I'll share some insights on how these tools were built.

Omatsuri landing

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 does exactly that, the only thing we need to do – add it to the production build in webpack config:

new OfflinePlugin({ autoUpdate: true, appShell: '/', excludes: ['404.html', 'CNAME'] }),
Enter fullscreen mode Exit fullscreen mode

Now we are ready to listen to service worker ready state and propose user to install PWA when it's done loading:

useEffect(() => {
  navigator.serviceWorker.ready
    .then(() => setOffline({ ready: true, error: false }))
    .catch(() => setOffline({ ready: false, error: true }));
}, []);
Enter fullscreen mode Exit fullscreen mode

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

SVG compression

There is actually only one good library for SVG compression (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 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 that will handle all worker bundling for you.

Here is an example of web worker usage for transforming svg to jsx with prettier and svg compression:

// 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 }));
};
Enter fullscreen mode Exit fullscreen mode
// 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 (/* ... */);
}
Enter fullscreen mode Exit fullscreen mode

Dark theme support

By default Omatsuri uses system theme, to listen to those changes, I've created react hook that returns current browser theme:

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;
}
Enter fullscreen mode Exit fullscreen mode

It's not enough though, since I wanted to give an option to change theme. To achieve that I've created ThemeProvider component 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:

const [theme] = useTheme();
Enter fullscreen mode Exit fullscreen mode

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 – 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 – 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!

Top comments (3)

Collapse
 
zarehba profile image
zarehba

That's a damn underrated post.
Amazing job. So polished.
Bookmarked. Loved it. Page divider and keyboard are my fav.

Collapse
 
maximwheatley profile image
Maxim Wheatley

This is super impressive, Vitaly! Thanks for writing up such a detailed post. Do you have any other OSS projects planned for 2021? Looking forward to following!

Collapse
 
djibe profile image
djibe

Awesome work !

Wonderful collection of must-have utilities.
Thanks a lot.