Author Archive

Migrating a Flow typed React SPA project from create-react-app (and Webpack) to Vite

I had a project based on Create React App, using Flow types, which I wanted to migrate to Vite. These are roughly the steps I had to follow:

  1. Most of my JSX files had a .js file extension. I renamed them all to .jsx – Vite is much happier that way
  2. I installed Vite and a few plugins:
    • @bunchtogether/vite-plugin-flow (to allow working with Flow types)
    • @vitejs/plugin-react
    • vite-jsconfig-paths
    • @vitejs/plugin-react-refresh
    • vite-plugin-svgr
yarn install -D vite vite-jsconfig-paths vite-plugin-svgr @vitejs/plugin-react @vitejs/plugin-react-refresh @bunchtogether/vite-plugin-flow
  1. Added a vite.config.js to the project root:
/* eslint-disable flowtype/require-valid-file-annotation */

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgrPlugin from "vite-plugin-svgr";
import { flowPlugin, esbuildFlowPlugin } from "@bunchtogether/vite-plugin-flow";
import jsconfigPaths from "vite-jsconfig-paths";
import * as path from "path";

// https://vitejs.dev/config/
/** @type {import('vite').UserConfig} */
export default defineConfig({
  optimizeDeps: {
    esbuildOptions: {
      plugins: [esbuildFlowPlugin()],
    },
  },
  plugins: [
    flowPlugin(), react(), jsconfigPaths(), svgrPlugin()
  ],
  resolve: {
    alias: [
      { find: "styles", replacement: path.resolve(__dirname, "src/styles") },
    ],
  },
});
  1. Added a new file at /flow-typed/vite_import_meta.js to handle Vite’s handling of environment variables:
// @flow
declare interface Import$Meta extends Import$Meta {
  env: {
    [key: string]: string,
  };
}
  1. Update the paths in my .jsconfig file:
{
  "compilerOptions": {
    "baseUrl": "src",
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "paths": {
      "flow-types": ["flow-types.js"],
      "polyfills": ["polyfills.js"],
      "apis/": ["apis/"],
      "App/": ["App/"],
      "assets/": ["assets/"],
      "constants/": ["constants/"],
      "errors/": ["errors/"],
      "hoc/": ["hoc/"],
      "hooks/": ["hooks/"],
      "redux/": ["redux/"],
      "styles/": ["styles/"],
      "utils/": ["utils/"],
      "validators/": ["validators/"]
    }
  },
  "include": ["src"],
  "exclude": [
    "node_modules",
    ".storybook",
    "Auth0",
    "flow-typed",
    "dist",
    ".idea"
  ]
}
  1. Change the prefix on all .env variables from REACT_APP_ to VITE_
  2. [ ] Change all instances of process.env["REACT_APP_WHATEVER”] to import.meta.env.VITE_WHATEVER
  3. Move index.html from public to the project root, and update any URLs to use %VITE_WHATEVER%
  4. Add this to the ignore section of your .flowconfig:
    /vite.config.js
  5. I ran into issues with circular references. I was able to find them with: npx madge src/index.jsx --circular
  6. Test like hell!

Thanks to Cathal Mac Donnacha, whose post about migrating a typescript project from CRA to Vite was hugely helpful.

This is my jam

Using Puppeteer 19 on Google Cloud Functions

I have a Google Cloud Function which uses Puppeteer to convert HTML documents to PDF. I recently tried to upgrade Puppeteer to version 19. It worked fine locally, but not when deployed to Google Cloud:

Error: Could not find Chromium (rev. 1069273). This can occur if either
1. you did not perform an installation before running the script (e.g. `npm install`) or
2. your cache path is incorrectly configured (which is: /root/.cache/puppeteer).
For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.

The fix was relatively straightforward:

First, add a new .puppeteerrc.cjs file at the root of the project (documented here):

const { join } = require("path");

/**
 * @type {import("puppeteer").Configuration}
 */
module.exports = {
  cacheDirectory: join(__dirname, ".cache", "puppeteer"),
};

Second, add .cache to your .gitignore file:

# Puppeteer cache
.cache

This fixed everything for my first deployment, but it broke again on subsequent deployments. This lead to Step 3: Add a new gcp-build item to the scripts object in your project’s package.json (documented here). This re-installs puppeteer after every deployment:

"scripts": {
  "gcp-build": "node node_modules/puppeteer/install.js",
  /* etc */
}

(Thanks to Justus Blümer for writing up the last part).

UROC Secures Ongoing Land Access Agreements

United Riders of Cumberland (“UROC”) has reached a landmark land access and management framework in order for the community and visitors to continue to enjoy access to the trails on the private and municipal lands surrounding Cumberland, BC.

https://unitedridersofcumberland.com/uroc-secures-ongoing-land-access-agreements/

A nice bit of news to brighten up a wet and rainy Tuesday morning.

Overcast is discontinuing the website player

I’ve decided to discontinue most of the web player’s functionality in 2023 and focus exclusively on the Overcast app for iPhone, iPad, Apple Watch, and M1/M2-powered Macs.

Marco Arment – https://overcast.fm/webplayerdiscontinuation

Sigh. I understand Marco’s rationale, but it’s still disappointing – I use the website player quite a lot.

Update: It’s not going away after all! 🙂

This is my jam

When a Transition component never reaches the exited state in React 18

After upgrading to React 18, I ran into an issue where a Transition component would never reach the exited state. Specifically it happens when navigating to a new route whose components were loaded using Suspense and React.lazy.

As is typical, I found someone else facing a very similar issue, but no workaround or fix was posted.

For me the answer was to give the Transition component a unique key prop, e.g.

const Example = () => {
  // Pull a key from Reach Router
  const { key } = useLocation();

  return (
    <Transition
      key={key} 
      // etc...

Et voila! The transitions work properly again.

Event handling with react-signature-pad-wrapper

I have a React app which makes use of react-signature-pad-wrapper (which is a React wrapper for signature_pad). My component needs to call a function whenever someone stops drawing on the canvas. Until recently, I could use the onEnd event like so:

const signaturePadOptions = {
  minWidth: 1,
  maxWidth: 5,
  penColor: "rgb(0, 0, 0)",
  // This part doesn't work anymore:
  onEnd: handleEndStroke, 
}

Whenever the onEnd event was fired, my handleEndStroke function was called. But that changed in version 4 of signature-pad: Now we have to listen for endStroke events on the signaturePad. I’ve used the useEffect hook with addEventListener in my React component:

useEffect(() => {
  const handleEndStroke = () => {
    // I do my custom stuff here
  };

  if (!!ref.current && !!ref.current.signaturePad) {
    const current = ref.current;
    // initiate the event handler
    current.signaturePad.addEventListener(
      "endStroke",
      handleEndStroke,
      false
    );
    // clean up the event handler
    return function cleanup() {
      current.signaturePad.removeEventListener(
        "endStroke",
        handleEndStroke
      );
    };
  }
}, [ref]);