Using Vite with React

vite-logo.svg

Background

It all started when I had to configure another simple thing in webpack (4.x). It’s a SPA monorepo app written with React, TypeScript, and styled-components. It takes ~2 minutes to start the dev server for the UI. Which is kind of frustrating. The fast-refresh is also not that fast, and you can see a notable delay between updates.

Understanding that migration to managed SSR solutions like Next.js takes more time, effort, and a stiff learning curve for my team, I had an opportunity to integrate one of the new shiny bundle tools that use esbuild + ES modules (ESM) with almost not config. esbuild is “An extremely fast JavaScript bundler and minifier”. EXTREMELY FAST:

Vite benchmarks

Why it’s so fast? because it’s written in go and do a lot of optimizations.
ES modules is a new browser feature that enables to natively load modules - meaning no need to bundle everything in one giant bundle. We can split the code into smaller modules and load them as needed. This helps avoid unnecessary work and stay fast no matter how big the project grows.

Native ESM dev server diagram

Bundle based dev server diagram

I already played with those tools with some little demos at home for my learning. But now I had to find a way to use them in the production app in my company.

Snowpack vs Vite vs WMR

It did not take a lot of time to choose the tool. Currently, there are 3 major bundlers powered by ESM - vite, snowpack, and wmr. From the first glance - all of them look promising and I did not care about their internals as long as I don’t need to touch them.

I chose Vite for 2 main reasons:

I think that in the near future - those issues will be resolve, but for now I bet on vite.

For deeper comparison, you can look at this article from vite’s documentation, but also notice that some of the latest snowpack progression does not mention there.

One of the benefits I like about those tools is that they require less configuration to write - they all support all the major workarounds out of the box. So you don’t tie into complex configs like in webpack. You don’t need to config anything to support TypeScript, React, Babel, manage public or static assets, fast-refresh, dynamic imports, etc… This makes migration easy if we will want.

The code you don’t have to write - is the code that doesn’t lift you from migration. It’s not a major bet if you don’t have to risk much.

The journey to configure vite

Looking at the example at vite-create-app ts-react template I was able to configure 80% of the work.

But I already know that it’s one of those cases that those last 20% take more time to configure than the first 80%. Those tiny bugs / unmatched stuff you have to fix. Some of them are:

  • I had to fix some unrelated TypeScript type definitions. I don’t know why do we have code like const f = <T,>() => return f2<T>() and why vite (or it may be a rollup / TypeScript issue?) struggles to transpile it - I convert this for now to <T extends unknown> which is equivalent in order to make it work.
  • We already have configured path aliases with webpack and tsconfig - so I wanted to keep that behavior. Luckily we have the vite-tsconfig-paths exactly for that. It worked well. I could also try using the alias for that.
  • Instead of webpack svgr - vite has a community plugin vite-plugin-svgr. Looking at its source code I can tell that it does the work as needed. In the future I may want to add more SVGR options (currently for custom colors we use css currentColor property), so I may contribute it or create a similar plugin. NOTE: vite bundles the static svg in addition assets even if you use vite-plugin-svgr. So many useless files will be generated. This is a minor issue that can be unnoticeable. I created a script for deleting those files (it has 0 impacts on the bundle because the client does not download those chunks).
  • Vite docs recommended specifying vite/client types in tsconfig.json. Before we did not explicitly specify this property - so we have to include other needed types like jest and node
    {
      "compilerOptions": {
        "types": ["vite/client"]
      }
    }
    

Issues with 3rd party libraries

  • Small issue with react-query devtools that solved by changing to a different minor version. But it’s more an issue with the library.
  • react-markdown v4 also has an issue using process.cwd internally. I upgraded to v5 and the problem is gone. It seems that like in webpack 5 - you won’t get and node.js polyfills with vite.

Now for the harder problems:

  • Some of our components depend on process.env.NODE_ENV. Our old webpack config uses define-plugin to replace them with their node.js env values. So I specify them with define:
    define: {
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
    }
    
  • Some third-party libraries like nivo for graphs use internally Node.js specific global keyword (here). But ES modules do not support it - and we get a Reference not found error for global variable. What vite tries to do is to convert them to be ESM - and run them in the browser. But the browser does not know what global is - so it fails. At first I try to handle this in vite.config.ts with define - setting global: {} . But doing so created a problem with import GlobalStyles from "./global.style" then converted to be unwanted import GlobalStyles from "./{}.style" . So until nivo will upgrade to ES-modules - I hacked that global variable by pre-load in index.html like
    <script>
      window.global = {};
    </script>
    <script src="./src/index.tsx" />
    

Env variables

The next thing I had to handle is the env variables. vite put the env variables inside import.meta.env (did you know import.meta is in the spec + node.js compatible?). Like next.js - if you want to expose them to the client-side you can use the VITE_APP_YOUR_VARIABLE_NAME - which is nice. For TypeScript users - vite docs encourage to add to tsconfig.json’s compilerOptions with types:["vite/client"] . But explicit writing types entry - you lose your jest, testing-library and node typings library supports. So you have to manually specify them too. You can now get vite typings - but they are still missing your env variables types. You can declare them like here.

I thinking to myself if it’s better to just use dotenv + env variables that like I did with define?

It makes stuff simplified.

Jest

I had a problem with our tests with ts-jest preset that did not understand what import.meta is. So I kept using process.env in our code instead of import.meta. This may not be a concern for you because it looks like vite is planning to create its own jest preset to solve this problem.

Bundle analyze

In our “old” build system - I configured webpack-bundle-analyze to inspect bundle chunks and understand what it made of. I added rollup-bundle-visualyzer instead (although there is an issue that the reported size is not correct).

The final vite.config.ts

import reactRefresh from "@vitejs/plugin-react-refresh";
import bundleVisualizer from "rollup-plugin-visualizer";
import { defineConfig } from "vite";
import svgr from "vite-plugin-svgr";
import tsconfigPaths from "vite-tsconfig-paths";

function getBundleVisualizerPlugin() {
  return {
    ...bundleVisualizer({
      template: "treemap", // or sunburst
      open: true,
      gzipSize: true,
      brotliSize: true,
    }),
    apply: "build",
    enforce: "post",
  } as Plugin;
}

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    reactRefresh(),
    svgr(),
    tsconfigPaths(),
    process.env.BUNDLE_ANALYZE === "1" && getBundleVisualizerPlugin(),
  ].filter(Boolean),
  define: {
    "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
  },
});
  • I will change the as Plugin type assertion when my PR to @types/rollup-bundle-visualyzer will be merged

Summary

All the struggle to config vite since the moment I took the decision till the moment my PR merged took one day. Now we have a fast development environment for improved productivity. I’m sure that not all 3rd party libraries are compatible with vite. But things are getting better. The tech world evolves and I hope that library authors will eventually supply maintain ESM compatible build.
One thing I recommend to do after the decision to use the tool - is to read the docs in depth. Vite has really good documentation. Skim https://github.com/vitejs/awesome-vite too. If you are early adopters like me - look at the open issues too.

My conclusion is that if you won’t need SSR (which experimentally supported in vite), I encourage you to try vite / other ESM build tools. It will make your dev environment really fast.