Skip to content

Instantly share code, notes, and snippets.

@NicolasBuecher
Last active May 30, 2025 23:44
Show Gist options
  • Save NicolasBuecher/6fc2eb9f942c02531075fd78f5bbfa23 to your computer and use it in GitHub Desktop.
Save NicolasBuecher/6fc2eb9f942c02531075fd78f5bbfa23 to your computer and use it in GitHub Desktop.
All necessary resources to set up, build and deploy a new React.js project with git, npm, VSCode, TypeScript, Vite, Vitest, Testing Library, ESLint, commitLint, husky, GitHub Actions CI/CD, Vercel and auto semantic releases in December 2022.

Set up a React + Vite + TypeScript project

This guide will give you all the instructions needed to set up a React.js project with git, npm, VSCode, TypeScript, Vite, Vitest, Testing Library, ESLint, commitlint, husky, GitHub Actions CI/CD, Vercel and auto semantic releases.

I recommend to follow these instructions in the order but I made them as independent as possible so you could skip one if you wanted to without struggling too much.

You may want to fork the already setup project here:

https://github.com/NicolasBuecher/react-vite-typescript-starter-project

TODO: Maybe add instructions for Docker, Storybook, CSS frameworks?

Summary

Why Vite?

Vite is a frontend dev/build tool based on Rollup module bundler which leverages the availability of native ES modules in the browser to improve the development experience.

In short: it's fast and we like that.

Why not mixing it with Next.js?

Next.js already has its own dev/build tools based on SWC compiler and bundler, they would overlap.

When should I use Vite?

Vite is like CRA, but faster. No complicated features are provided. Pick it if you don't really mind about SEO, SSR or SSG.

Otherwise, I suggest you to take a look at my React + Next + TypeScript guide.

Initialize a git repository

git init
git commit --allow-empty -m "Initial commit"

In parallel you can create a new remote repository on GitHub at https://github.com/new.

And then link your local repository to your remote repository:

git branch -M main
git remote add origin git@github.com:<USER_NAME>/<REPOSITORY_NAME>.git
git push -u origin main

Initialize a React + Vite + TypeScript project

npm create vite@latest . -- --template react-ts
npm install

Commit your changes:

git add .
git commit -m "build: Initialize react project"

Create a README.md file

Create a README.md file at the root of the project.

Commit your changes:

git add README.md
git commit -m "docs: Create readme file"

Configure ESLint

The common way to initialize an ESLint configuration is to run the command:

npm init @eslint/config

But it currently sucks. I recommend you to copy/paste this optimized .eslintrc.json file at the root of your project. This is an opinionated configuration, you may remove the rules part to stick to the most conventional rules.

In this case, you also need to install the config dependencies:

npx install-peerdeps --dev eslint-config-airbnb
npx install-peerdeps --dev eslint-config-airbnb-typescript
npm install --save-dev eslint-plugin-testing-library

Add a .eslintignore file at the root of your project:

node_modules
dist

Update your package.json with a lint script:

"scripts": {
  "lint": "eslint 'src/**/*.ts{,x}'",
  "lint:fix": "npm run lint -- --fix"
}

You may format your code following your new linting rules before continuing:

npm run lint:fix

Commit your changes:

git add .eslintrc.json .eslintignore src/ package.json package-lock.json
git commit -m "build: Configure eslint"

Configure VSCode auto format

Update your VSCode workspace settings with:

{
  "editor.defaultFormatter": "dbaeumer.vscode-eslint",
  "editor.formatOnSave": true,
  "eslint.alwaysShowStatus": true,
  "editor.codeActionsOnSave": {
      "source.fixAll.eslint": true
  },
}

Or copy/paste this custom settings.json file in .vscode/.

Configure Vitest

Install Vitest and React Testing Library packages:

npm install vitest jsdom @vitest/ui @testing-library/react @testing-library/jest-dom @testing-library/user-event --save-dev

Add a setup.ts file at src/test/:

import "@testing-library/jest-dom";

Update vite.config.ts:

/// <reference types="vitest" />

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: "jsdom",
    setupFiles: "./src/test/setup.ts"
  }
})

Update your package.json file with test scripts:

"scripts": {
  "test": "vitest run",
  "test:ui": "vitest --ui",
  "test:watch": "vitest"
}

Create a App.test.tsx test file along App.tsx file:

import { render, screen } from "@testing-library/react";
import user from "@testing-library/user-event";
import App from "./App";

describe("App", () => {
  describe("when button is clicked", () => {
    it("should increment the counter", async () => {
      render(<App />);
      user.click(screen.getByRole("button"));
      expect(await screen.findByText(/count is 1/i)).toBeInTheDocument();
    });
  });
});

Commit your changes:

git add src/ vite.config.ts package.json package-lock.json
git commit -m "build: Configure vitest"

Configure git hooks with husky

Use husky-init to set up husky:

npx husky-init && npm install

Update the default pre-commit hook to run ESLint:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run lint
npm run test

Install commitlint with Angular config:

npm install @commitlint/{cli,config-angular} --save-dev

Create a .commitlintrc.json file at the root of your project and configure commitlint to use Angular config:

{
  "extends": ["@commitlint/config-angular"]
}

Or, if you like Angular configuration but prefer using sentence-case (first letter uppercase) for the subject, you can copy/paste this .commitlintrc.json config file at the root of your project. In this guide, I'm using this opinionated configuration.

Create a commit-msg hook:

npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

Commit your changes:

git add .husky/ .commitlintrc.json package.json package-lock.json
git commit -m "build: Configure git hooks with husky"

Create a CI/CD GitHub Actions workflow

1. Generate an access token

On GitHub main page, go to Settings -> Developer settings -> Personal access tokens -> Tokens (classic) or directly to https://github.com/settings/tokens.

Generate a new token, name it <project-name>-release-access and copy it:

  • For a public repo, select the public_repo scope
  • For a private repo, select the parent repo scope

2. Add the secret to your remote repository

On your GitHub repository page, go to Settings -> Secrets -> Actions.

Create a new repository secret, name it ACTIONS_RELEASE_ACCESS_TOKEN and paste the generated token into the Secret body.

3. Configure the semantic release

Install semantic-release package:

npm install semantic-release --save-dev

Create a .releaserc config file at the root of your project:

branches:
  - main
debug: false
ci: true
dryRun: false
plugins:
  - "@semantic-release/commit-analyzer"
  - "@semantic-release/release-notes-generator"
  - "@semantic-release/github"

4. Create the CI/CD workflow

Create a ci.yml file in .github/workflows/. You can copy/paste this custom ci.yml file.

5. Commit your changes

git add .github/ .releaserc package.json package-lock.json
git commit -m "ci: Configure actions ci workflow"

Deploy your project on Vercel

Create a free account on Vercel.

Keep the default settings and import your project into Vercel. The repository needs to be public for Vercel to deploy it.

After your project has been imported and deployed, all subsequent pushes to branches will generate preview deployments, and all changes made to the production branch (commonly “main”) will result in a production deployment.

Feel free to create a develop branch to not push directly into the main production branch.

{
"extends": ["@commitlint/config-angular"],
"rules": {
"body-leading-blank": [2, "always"],
"footer-leading-blank": [2, "always"],
"subject-case": [
2,
"always",
["sentence-case"]
]
}
}
{
// Prevent ESLint from looking for other configurations in parent directories
"root": true,
// Enable the use of global variables
"env": {
// From browser environment
"browser": true,
// From every ECMAScript release until ES2021 (because not all ES2022 features are supported on modern browsers)
"es2021": true
},
// Extend existing configurations
// Order matters, each extension overrides the previous one
"extends": [
// Airbnb config uses import, jsx-a11y and react plugins
"airbnb",
// Airbnb hooks config uses react-hooks plugin
"airbnb/hooks",
// Airbnb typescript config uses @typescript-eslint plugin and overrides airbnb config
"airbnb-typescript",
// Enable the use of the new JSX transform from React 17
"plugin:react/jsx-runtime"
],
"overrides": [
{
// Run testing rules only against test files
"files": ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"],
"extends": ["plugin:testing-library/react"],
"rules": {
// Allow nesting describe/test callbacks in Jest tests
"max-nested-callbacks": "off",
// Allow magic numbers in tests
"no-magic-numbers": "off"
}
}
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
// Link the typescript parser to the local typescript configuration
"project": "./tsconfig.json",
// Enable the use of syntax from latest ECMAScript releases
"ecmaVersion": "latest",
// Enable the use of syntax from ECMAScript modules
"sourceType": "module"
},
"ignorePatterns": ["vite.config.ts", "src/test/setup.ts"],
"rules": {
// Enforce consistent use of double quotes instead of single quotes to define strings
// IMHO having to escape single quotes is more annoying than escaping double quotes because
// single quotes are mandatory for contractions though double quotes never are mandatory
// See http://stackoverflow.com/a/18041188/1480391
// https://typescript-eslint.io/rules/quotes/
"quotes": "off",
"@typescript-eslint/quotes": [
"error",
"double",
{
"avoidEscape": true
}
],
// Enforce a convention in module import order
// https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/order.md
"import/order": [
"error",
{
"newlines-between": "never",
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index"
],
"alphabetize": {
"order": "asc",
"caseInsensitive": false
}
}
],
// Enforce the maximum depth callbacks that can be nested
// https://eslint.org/docs/latest/rules/max-nested-callbacks
"max-nested-callbacks" : ["error", 4],
// Enforce the use of explicit constants instead of meaningless numbers
// https://eslint.org/docs/rules/no-magic-numbers
"no-magic-numbers": [
"error",
{
"ignore": [
-1,
0,
1,
2,
3,
10,
100,
1000
],
"ignoreArrayIndexes": true,
"enforceConst": true,
"detectObjects": false
}
]
}
}
name: CI & Release
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
timeout-minutes: 10
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [19.x]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Node.js ${{ matrix.node-version }} set up
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Dependencies install
run: npm ci
- name: Test
run: npm run test --if-present
- name: Lint
run: npm run lint --if-present
- name: Build
run: npm run build --if-present
- name: Release
run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.ACTIONS_RELEASE_ACCESS_TOKEN }}
{
// Display .git folder in the workspace
"files.exclude": {
"**/.git": false
},
// Set indentation to 2 whitespaces
"editor.tabSize": 2,
// Add a vertical line to visualize the characters number limit
"editor.rulers": [100],
// Next block controls eslint autoformat on save
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.formatOnSave": true,
"eslint.alwaysShowStatus": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment