How to Set Up a React Project with Biome, Vitest, and GitHub Actions in 10 Minutes

The default React project setup in 2026 no longer needs ESLint, Prettier, and Jest as three separate dependencies with three separate config files that somehow still conflict with each other. Biome handles linting and formatting in one tool. Vitest runs your tests faster than Jest ever did. GitHub Actions ties it together. Here's the exact react project setup for 2026 that gets you from zero to a working CI pipeline in about ten minutes.

I've used this stack on four production projects now. The DX improvement over the old ESLint + Prettier + Jest trinity is significant — fewer dependencies, faster feedback loops, and dramatically less config.

What You're Building

By the end of this guide you'll have:

  • A React + TypeScript project scaffolded with Vite
  • Biome for linting and formatting (replacing ESLint and Prettier)
  • Vitest with React Testing Library for unit and component tests
  • A GitHub Actions workflow that runs lint, format check, and tests on every push

Total config files: 3. Total setup time: genuinely about 10 minutes if you type at a reasonable speed.

Step 1: Scaffold the Project with Vite

If you haven't already picked a bundler, use Vite. It's the most mature option with the best plugin ecosystem — we covered this in detail in our Vite vs Turbopack vs Rspack comparison.

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install

That gives you a clean React + TypeScript project. Vite's template ships with a basic tsconfig.json and a dev server that starts in under 200ms. Delete the default CSS and boilerplate if you want, but it doesn't matter for our purposes.

Step 2: Install and Configure Biome

This is where things get good. One install, one config file, and you've replaced both ESLint and Prettier.

npm install --save-dev --save-exact @biomejs/biome
npx @biomejs/biome init

That creates a biome.json at your project root. Here's the config I use on every project — it's opinionated, but these defaults are sensible:

{
  "$schema": "https://biomejs.dev/schemas/2.0/schema.json",
  "organizeImports": {
    "enabled": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "correctness": {
        "noUnusedVariables": "warn",
        "useExhaustiveDependencies": "warn"
      },
      "suspicious": {
        "noExplicitAny": "warn"
      }
    }
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single",
      "semicolons": "always"
    }
  }
}

A few notes. The useExhaustiveDependencies rule catches the same stale-closure bugs as the old eslint-plugin-react-hooks rule — Biome has built-in React support, no plugins needed. The noExplicitAny as a warning rather than error is a pragmatic choice; sometimes you need an escape hatch during rapid prototyping.

Add these scripts to your package.json:

"scripts": {
  "lint": "biome check .",
  "lint:fix": "biome check --write .",
  "format": "biome format .",
  "format:fix": "biome format --write ."
}

Run npm run lint. It'll flag issues in the Vite boilerplate code instantly. Biome checks both lint rules and formatting in a single pass — it's written in Rust, and it shows. On a medium-sized codebase (500 files), it finishes before you can switch to the terminal tab to check. If you're using WebStorm, there's a first-party Biome plugin that gives you real-time feedback as you type.

Now remove ESLint if Vite scaffolded it (newer templates might include it):

npm uninstall eslint @eslint/js eslint-plugin-react-hooks eslint-plugin-react-refresh
rm eslint.config.js

One tool. One config. Done.

Step 3: Set Up Vitest with React Testing Library

Vitest uses the same config as Vite, which means zero bundler configuration. It just works with your existing vite.config.ts. That sentence alone should sell you on switching from Jest.

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

Update your vite.config.ts:

/// <reference types="vitest/config" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/test/setup.ts',
    css: true,
  },
});

Create the setup file at src/test/setup.ts:

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

And update tsconfig.json (or your tsconfig.app.json if Vite split them) to include the Vitest types:

"compilerOptions": {
  "types": ["vitest/globals"]
}

Write a quick smoke test to confirm everything works. Create src/App.test.tsx:

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

describe('App', () => {
  it('renders without crashing', () => {
    render(<App />);
    expect(screen.getByRole('main') || document.querySelector('#root')).toBeTruthy();
  });
});

Add the test script to package.json:

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

Run npm test. It should pass in under a second. Vitest's watch mode (npm run test:watch) is particularly impressive — it uses Vite's module graph to only re-run tests affected by your changes. No more waiting 30 seconds for your entire Jest suite to crawl through unrelated files.

Step 4: Wire Up GitHub Actions

Create .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'npm'

      - run: npm ci

      - name: Lint and format check
        run: npx biome ci .

      - name: Run tests
        run: npm test

      - name: Type check
        run: npx tsc --noEmit

Note the biome ci command — it's different from biome check. The ci command is designed for pipelines: it outputs GitHub-compatible annotations, produces a non-zero exit code on any violation, and formats its output for readability in CI logs. This is one of those things the docs don't make obvious enough.

The workflow runs lint, format check, tests, and type checking in a single job. For a small-to-medium project, this finishes in under 90 seconds. If your project grows and CI time becomes a concern, you can split these into parallel jobs, but don't optimise prematurely.

Push it up and you've got a working CI pipeline.

Step 5: Add a Pre-commit Hook (Optional but Recommended)

CI catches problems, but catching them before you push is better. Use lefthook — it's faster and simpler than husky + lint-staged:

npm install --save-dev lefthook
npx lefthook install

Create lefthook.yml:

pre-commit:
  commands:
    lint:
      glob: '*.{js,ts,jsx,tsx,json}'
      run: npx biome check --write --staged {staged_files}
      stage_fixed: true

This runs Biome on staged files only, auto-fixes what it can, and re-stages the corrected files. Fast and non-intrusive.

The Full Picture

Here's what your project structure looks like:

my-app/
├── .github/workflows/ci.yml
├── biome.json
├── lefthook.yml
├── vite.config.ts
├── src/
│   ├── App.tsx
│   ├── App.test.tsx
│   └── test/setup.ts
├── package.json
└── tsconfig.json

Three config files for the entire toolchain (four if you count the CI workflow). Compare that to the typical ESLint + Prettier + Jest setup: .eslintrc, .eslintignore, .prettierrc, .prettierignore, jest.config.js, babel.config.js for Jest's transform... it adds up.

Where to Deploy

Once CI is green, you need somewhere to ship. For React SPAs, Vercel is the path of least resistance — connect your GitHub repo, and it deploys on every merge to main with zero config. If you need more control over your infrastructure, Netlify gives you similar DX with more flexibility around build settings and redirects.

For production apps, add Sentry early. You don't want to find out about runtime errors from your users. Their React SDK takes about five minutes to integrate and catches unhandled exceptions, performance regressions, and replay sessions so you can actually debug what happened.

Wrapping Up

This is the React project setup I reach for on every new project in 2026. Biome replaces two tools with one and runs an order of magnitude faster. Vitest gives you Jest-compatible testing with zero extra bundler config. GitHub Actions glues it together.

The old stack still works. Nobody's going to break into your house for using ESLint. But if you're starting fresh, there's no reason to carry that config baggage. This setup is faster to configure, faster to run, and easier to maintain. Ship it.

H