I'm happy to announce that I've finished development of Omatsuri – open source React PWA that includes 12 Frontend focused tools. In this post I'll share some insights on how these tools were built.
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'] }),
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 }));
}, []);
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.
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 }));
};
// 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 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;
}
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();
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)
That's a damn underrated post.
Amazing job. So polished.
Bookmarked. Loved it. Page divider and keyboard are my fav.
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!
Awesome work !
Wonderful collection of must-have utilities.
Thanks a lot.