Porting a Create React App application to Vite Pt. 1: Base Project
Table of Contents
Why?#
Vite’s primary value proposition is a faster, more performant developer experience. Vite is powered by Rollup and ESBuild, a JavaScript bundler written in Go, while Create React App leverages the stable, battle tested, combination of WebPack and the Babel transpiler.
So, if I am happy with Create React App then why this port?
- Evaluate the Hype - I like to try out the new tech and see if it can improve my workflow
- Pet Peeve - CRA does not give you full control of your Jest configuration, plus it pollutes
package.json
with the configuration values that you are allowed to specify - To do it - I learn a lot from little experiments like this
I am breaking this into two parts in an attempt to keep the information digestible. This first part will focus on getting the application to run in the browser, identical to the CRA version, along with the same quality assurance tooling that mostly comes out of the box with CRA. Part two will focus on unit tests and enforcing test coverage.
As always use your own judgement before pursuing the next-shiny-thing, because for all I know CRA may soon be able to leverage the new TurboPack proposed WebPack successor and re-claim the speed title.
Preflight#
Create a Clean Shell#
Let’s begin by scaffolding the new project from Vite’s minimal React + TypeScript template:
yarn create vite vite-redux-seed --template react-ts
cd vite-redux-seed
yarn
Add Static Code Analysis#
Setting up some basic static analysis tooling is a fairly easy, and inexpensive, way of ensuring basic code quality and consistency. As an added bonus there are plugins for ESLint such as jsx-a11y to alert me when I am inadvertently creating accessibility issues in my application. I highly recommend the typesync command to save time by automatically finding any missing typings for your packages.
yarn add --dev npm-run-all browserslist sass prettier eslint stylelint \
@typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier \
eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks \
stylelint-config-prettier stylelint-config-recommended-scss stylelint-scss \
postcss
# add missing typings to package.json
npx typesync
# install the typings found by typesync
yarn
Now we can pull most of our code-quality configuration files straight over from the old project:
- .browserslistrc : browsers we are promising to support
- .editorconfig : basic text editor settings such as default line endings
- .eslintignore : files/folders we do not want ESLint to analyze
- .prettierrc : source code format rules
- .prettierignore : files/folders Prettier should ignore
- .stylelintrc.json : stylesheet format rules
Create React App sets many defaults for ESLint that we will have to explicitly mimic ourselves in a new custom ESLint config file:
file: .eslintrc.json
{
"root": true,
"env": {
"browser": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "react", "react-hooks", "jsx-a11y"],
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended",
"prettier"
],
"rules": {
"no-magic-numbers": [2, { "ignore": [-1, 0, 1, 2, 10, 100, 1000] }],
"no-unused-vars": [2, { "vars": "local", "args": "after-used", "argsIgnorePattern": "_" }],
"no-console": [
"error",
{
"allow": ["error", "info", "warn"]
}
]
},
"settings": {
"react": {
"version": "detect"
}
}
}
Now that our tools are installed and configured let’s artificially create an accessibility error by removing an alt property from our image tag and verify it causes a linting error:
As we can now verify that the linter is working why don’t we take the next step and explicitly enforce our choices with a git hook. First we will need a couple of packages:
- Husky to easily create hooks for this project
- Pretty-Quick to automatically apply our predefined code formatting standards
yarn add --dev pretty-quick husky
npx husky install
npm pkg set scripts.prepare="husky install"
I have added a few scripts to help with project maintenance - the two highlighted below will ensure that all code is in our defined format, and that any linting issues will cause an error:
file: package.json
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"format": "pretty-quick --staged",
"format:check": "prettier --check .",
"format:fix": "prettier --write .",
"lint": "run-p lint:*",
"lint:ts": "eslint ./src/**/**.ts*",
"lint:styles": "stylelint \"./src/**/*.scss\"",
"fix:styles": "stylelint \"./src/**/*.scss\" --fix",
"prepare": "husky install"
},
Once they are added to a pre-commit hook any commit that is attempted with style or code linting errors will automatically be aborted:
npx husky add .husky/pre-commit "npm run format"
npx husky add .husky/pre-commit "npm run lint"
Porting the Application#
Let’s first make sure that the project will run, by replacing the contents of the src
folder with everything
from the vanilla CRA project - excluding tests and snapshots for now. We will need to rename src/index.tsx
,
Create React App’s default root component, to src/main.tsx
which is Vite’s default. Before trying to run it we
need to install all of our missing packages:
yarn add @fortawesome/fontawesome-svg-core @fortawesome/free-brands-svg-icons \
@fortawesome/free-regular-svg-icons @fortawesome/free-solid-svg-icons \
@fortawesome/react-fontawesome @reduxjs/toolkit bootstrap @popperjs/core \
react-bootstrap react-redux react-router-dom redux uuid
# add missing typings to package.json
npx typesync
# install the typings found by typesync
yarn
.env Changes#
I utilized Create React App’s .env file to store a sample third party API URL:
file: .env (old app)
REACT_APP_API_URI=https://jsonplaceholder.typicode.com
It will need to be changed to Vite’s format:
file: .env
VITE_API_URI=https://jsonplaceholder.typicode.com
Let’s also add type information for our custom environment variable(s):
file: /src/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URI: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
And finally update the reference in the service helper:
file: /src/helpers/service.ts
...
const apiUri = process.env.REACT_APP_API_URI;
...
becomes:
file: /src/helpers/service.ts
...
const apiUri = import.meta.env.VITE_API_URI;
...
First Test Run#
Let’s see how far that has gotten us:
yarn dev
Fix Bootstrap’s Sass Import#
It appears that we need to tell Vite how to resolve ~bootstrap
from /src/styles/global.scss
:
file: vite.config.ts
import { resolve } from 'path';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'~bootstrap': resolve(__dirname, 'node_modules/bootstrap'),
},
},
});
But now I have angered WebStorm
Lucky for me this is an easy fix by adding the NodeJS typings to the project:
yarn add --dev @types/node
Second Test#
Now if we refresh or re-run yarn dev
the running application looks okay:
Quality Assurance Sweep#
Better verify what the linter sees:
➜ vite-redux-seed (main) ✗ yarn lint
yarn run v1.22.19
$ run-p lint:*
$ eslint ./src/**/**.ts*
$ stylelint "./src/**/*.scss"
/home/tgoshinski/work/lab/react/vite-redux-seed/src/@enums/AlertTypes.ts
1:6 error 'AlertTypes' is defined but never used no-unused-vars
2:3 error 'Error' is defined but never used no-unused-vars
3:3 error 'Info' is defined but never used no-unused-vars
4:3 error 'Success' is defined but never used no-unused-vars
5:3 error 'Warning' is defined but never used no-unused-vars
/home/tgoshinski/work/lab/react/vite-redux-seed/src/@enums/AppRoutes.ts
1:6 error 'AppRoutes' is defined but never used no-unused-vars
2:3 error 'Home' is defined but never used no-unused-vars
3:3 error 'Users' is defined but never used no-unused-vars
4:3 error 'Notifications' is defined but never used no-unused-vars
5:3 error 'Fallthrough' is defined but never used no-unused-vars
/home/tgoshinski/work/lab/react/vite-redux-seed/src/@enums/AsyncStates.ts
1:6 error 'AsyncStates' is defined but never used no-unused-vars
2:3 error 'Idle' is defined but never used no-unused-vars
3:3 error 'Pending' is defined but never used no-unused-vars
4:3 error 'Success' is defined but never used no-unused-vars
5:3 error 'Fail' is defined but never used no-unused-vars
/home/tgoshinski/work/lab/react/vite-redux-seed/src/@enums/FetchMethods.ts
1:6 error 'FetchMethods' is defined but never used no-unused-vars
2:3 error 'Delete' is defined but never used no-unused-vars
3:3 error 'Get' is defined but never used no-unused-vars
4:3 error 'Patch' is defined but never used no-unused-vars
5:3 error 'Post' is defined but never used no-unused-vars
6:3 error 'Put' is defined but never used no-unused-vars
/home/tgoshinski/work/lab/react/vite-redux-seed/src/@enums/HttpStatusCodes.ts
2:6 error 'HttpStatusCodes' is defined but never used no-unused-vars
3:3 error 'Ok' is defined but never used no-unused-vars
4:3 error 'Created' is defined but never used no-unused-vars
5:3 error 'Accepted' is defined but never used no-unused-vars
6:3 error 'NoContent' is defined but never used no-unused-vars
7:3 error 'MovedPermanently' is defined but never used no-unused-vars
8:3 error 'Redirect' is defined but never used no-unused-vars
9:3 error 'BadRequest' is defined but never used no-unused-vars
10:3 error 'Unauthorized' is defined but never used no-unused-vars
11:3 error 'Forbidden' is defined but never used no-unused-vars
12:3 error 'NotFound' is defined but never used no-unused-vars
13:3 error 'InternalServerError' is defined but never used no-unused-vars
14:3 error 'NotImplemented' is defined but never used no-unused-vars
15:3 error 'BadGateway' is defined but never used no-unused-vars
/home/tgoshinski/work/lab/react/vite-redux-seed/src/@enums/ToastTypes.ts
1:6 error 'ToastTypes' is defined but never used no-unused-vars
2:3 error 'Error' is defined but never used no-unused-vars
3:3 error 'Info' is defined but never used no-unused-vars
4:3 error 'Success' is defined but never used no-unused-vars
5:3 error 'Warning' is defined but never used no-unused-vars
/home/tgoshinski/work/lab/react/vite-redux-seed/src/helpers/service.ts
23:17 error 'RequestInit' is not defined no-undef
✖ 41 problems (41 errors, 0 warnings)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
ERROR: "lint:ts" exited with 1.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Ouch, that is a lot of errors. One thing I see that I forgot was to extend the ESLint TypeScript plugin’s recommended:
file: .eslintrc.json
{
"root": true,
"env": {
"browser": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "react", "react-hooks", "jsx-a11y"],
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"no-magic-numbers": [2, { "ignore": [-1, 0, 1, 2, 10, 100, 1000] }],
"no-unused-vars": [2, { "vars": "local", "args": "after-used", "argsIgnorePattern": "_" }],
"no-console": [
"error",
{
"allow": ["error", "info", "warn"]
}
]
},
"settings": {
"react": {
"version": "detect"
}
}
}
Hooray - more errors, not less:
/home/tgoshinski/work/lab/react/vite-redux-seed/src/@interfaces/IApiBaseResponse.ts
7:10 warning Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
/home/tgoshinski/work/lab/react/vite-redux-seed/src/helpers/service.ts
74:66 warning Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
✖ 42 problems (40 errors, 2 warnings)
Personally I choose to turn off no-explicit-any
since I do find them useful in very limited circumstances.
YMMV but I find that extraneous abuse of any
is best called out in code
reviews depending on the individual team norms. Next it seems that ESLint’s general no-unused-vars
is overriding
the TypeScript plugin’s no-unused-vars
which is causing all of my enumerations to fail. We should be able to fix these
with a couple of simple adjustments to our rules section:
file: .eslintrc.json
{
"root": true,
"env": {
"browser": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "react", "react-hooks", "jsx-a11y"],
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"no-magic-numbers": [2, { "ignore": [-1, 0, 1, 2, 10, 100, 1000] }],
"no-unused-vars": "off",
"no-console": [
"error",
{
"allow": ["error", "info", "warn"]
}
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": [
2,
{ "vars": "local", "args": "after-used", "argsIgnorePattern": "_" }
]
},
"settings": {
"react": {
"version": "detect"
}
}
}
One more time:
➜ vite-redux-seed (main) ✗ yarn lint
yarn run v1.22.19
$ run-p lint:*
$ stylelint "./src/**/*.scss"
$ eslint ./src/**/**.ts*
Done in 2.79s.
All better.
End of Part One#
Getting the application to run was not too difficult. It mostly involved reading a little documentation around Vite and around the ESLint settings that CRA hides with its custom plugins. Admittedly I am new to Vite so would appreciate any advice on something you think I could be doing better.