End-to-end React project setup for 2026.
Bootstrap
npm create vite@latest myapp -- --template react-ts
cd myapp
npm i
Dependencies
npm i react-router @tanstack/react-query zustand zod react-hook-form @hookform/resolvers
npm i -D tailwindcss postcss autoprefixer
npm i -D vitest @vitest/coverage-v8 jsdom \
@testing-library/react @testing-library/jest-dom @testing-library/user-event \
msw
npm i -D eslint @eslint/js typescript-eslint eslint-plugin-react eslint-plugin-react-hooks \
eslint-config-prettier prettier prettier-plugin-tailwindcss
npm i -D @tanstack/react-query-devtools @welldone-software/why-did-you-render
tsconfig.json
{
"compilerOptions": {
"target": "ES2023",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"esModuleInterop": true,
"skipLibCheck": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"resolveJsonModule": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"paths": { "@/*": ["./src/*"] }
},
"include": ["src", "test"]
}
vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "node:path";
export default defineConfig({
plugins: [react()],
resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
server: { port: 3000 },
});
Tailwind
npx tailwindcss init -p
tailwind.config.js:
export default {
content: ["./index.html", "./src/**/*.{ts,tsx}"],
theme: { extend: {} },
plugins: [],
};
src/index.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
.prettierrc
{
"semi": true,
"trailingComma": "all",
"singleQuote": false,
"printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss"]
}
eslint.config.js (flat)
import js from "@eslint/js";
import ts from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import prettier from "eslint-config-prettier";
export default ts.config(
js.configs.recommended,
...ts.configs.strictTypeChecked,
{
files: ["**/*.{ts,tsx}"],
plugins: { react, "react-hooks": reactHooks },
rules: {
...react.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
"react/react-in-jsx-scope": "off",
},
languageOptions: {
parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
},
settings: { react: { version: "detect" } },
},
prettier,
{ ignores: ["dist", "coverage", "node_modules"] },
);
vitest.config.ts
import { defineConfig, mergeConfig } from "vitest/config";
import viteConfig from "./vite.config";
export default mergeConfig(viteConfig, defineConfig({
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./test/setup.ts"],
coverage: { provider: "v8", reporter: ["text", "html"] },
},
}));
test/setup.ts:
import "@testing-library/jest-dom/vitest";
App structure
src/
├── main.tsx
├── App.tsx
├── routes/
│ ├── _root.tsx
│ ├── index.tsx
│ └── users/$id.tsx
├── components/
│ ├── ui/ # shadcn-style primitives
│ └── ...
├── hooks/
├── lib/
│ ├── api.ts
│ ├── auth.ts
│ └── env.ts
├── stores/
│ └── auth.ts
├── styles/
│ └── index.css
└── types/
src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter } from "react-router";
import App from "./App";
import "./styles/index.css";
const qc = new QueryClient({
defaultOptions: { queries: { staleTime: 60_000, refetchOnWindowFocus: false } },
});
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={qc}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
</React.StrictMode>,
);
src/lib/env.ts
import { z } from "zod";
const Env = z.object({
VITE_API_URL: z.string().url(),
});
export const env = Env.parse(import.meta.env);
package.json scripts
{
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"format": "prettier --write .",
"test": "vitest",
"test:run": "vitest run --coverage"
}
}
CI
.github/workflows/ci.yml:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20", cache: "npm" }
- run: npm ci
- run: npm run typecheck
- run: npm run lint
- run: npm run test:run
- run: npm run build
Pre-commit
npm i -D husky lint-staged
npx husky init
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{css,json,md}": ["prettier --write"]
}
}
.husky/pre-commit:
npx lint-staged
.gitignore
node_modules/
dist/
coverage/
*.log
.env
.DS_Store
Common conventions
- One folder per feature, not per file type.
- Co-locate tests next to source (
Component.tsx+Component.test.tsx). - Validate env at boundary (Zod).
- Strict TS from day one.
- StrictMode in dev.
- One QueryClient at app root.
Read this next
That’s 20 React cheatsheets. Next category: Next.js.
If you want my full React starter (Vite + Tailwind + Query + Router), it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .