Build Your First React App in 20 Minutes (Without Create React App)

Posted November 17, 2025 by Karol Polakowski

Create React App (CRA) is still useful, but modern workflows often prefer lighter, faster tools such as Vite, Parcel, or esbuild. In this guide you’ll build a minimal, production-ready React app using Vite as the dev server and build tool — no CRA, no opaque configuration, and no template generators. You’ll learn how files map to behavior, how to run dev and production builds, and common gotchas.


What you’ll need

  • Node.js 14+ (Node 18+ recommended)
  • npm or yarn
  • A code editor (VS Code recommended)

Goal

Create a simple React app with:

  • index.html entry
  • src/main.jsx and src/App.jsx
  • CSS file
  • npm scripts for dev, build, and preview

Step 1 — Create the project folder

Run in your terminal:

mkdir my-react-app && cd my-react-app
npm init -y

Step 2 — Install dependencies

Install React, ReactDOM, and Vite as a dev dependency:

npm install react react-dom
npm install -D vite

Step 3 — Add minimal npm scripts

In package.json add these scripts so you can run the dev server and build:

"scripts": {
  "dev": "vite",
  "build": "vite build",
  "preview": "vite preview --port 5000"
}

Step 4 — Create the HTML entry

Create an `index.html` at the project root. Vite serves this file and injects the module script to `src/main.jsx`.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My React App</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

Note: In the snippet above the JSX mount point is `<div id=”root”>` and the module script loads `src/main.jsx`.

Step 5 — Create the React entry and App component

Create `src/main.jsx` and `src/App.jsx`.

src/main.jsx

import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import './styles.css'

const container = document.getElementById('root')
const root = createRoot(container)
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

src/App.jsx

import React, { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0)
  return (
    <div className="app">
      <h1>Hello, React (Vite)!</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  )
}

Note: Angle brackets in JSX are escaped above using `<` and `>` so the code sample renders safely in the article.

Step 6 — Add a tiny stylesheet

Create `src/styles.css`:

body {
  font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
  margin: 0;
  padding: 2rem;
  background: #f5f7fb;
  color: #111827;
}

.app {
  max-width: 640px;
  margin: 0 auto;
  background: white;
  padding: 1.25rem;
  border-radius: 8px;
  box-shadow: 0 6px 18px rgba(17, 24, 39, 0.08);
}

Step 7 — Start the dev server

Run:

npm run dev

Vite will start on port 5173 by default. Open the printed URL (usually http://localhost:5173) and you should see your app.

Quick explanation: How this works

  • index.html is the app entry served by Vite.
  • `<script type=”module” src=”/src/main.jsx”>` tells the browser to load your module; Vite uses native ES modules in dev.
  • Vite handles JSX transforms and HMR (hot module replacement) with near-instant reloads.

Build for production

When you’re ready to produce an optimized build:

npm run build
npm run preview

`vite build` produces an optimized `dist` directory with hashed assets. `vite preview` serves the production build locally so you can validate it.


Optional: TypeScript

To use TypeScript, install TypeScript and types and rename files:

npm install -D typescript @types/react @types/react-dom
mv src/main.jsx src/main.tsx
mv src/App.jsx src/App.tsx
# add tsconfig.json (Vite will use it)

Vite has first-class TypeScript support — it delegates type checking to `tsc` but still handles transforms quickly.

Why use Vite instead of CRA?

  • Faster cold-start and HMR — Vite uses ES modules and on-demand transforms.
  • Smaller dev dependency surface and simpler configuration.
  • Production builds use Rollup underneath for good optimization.
  • You get more control if you want to extend the setup.

Alternatives

If you prefer zero-config bundling, Parcel is another great choice. For ultra-fast builds you can explore esbuild or Bun (runtime + bundler), but Vite offers a balanced developer experience and ecosystem.

Troubleshooting

  • Blank page: open dev console and ensure `root` element exists and your `script` path in `index.html` is exactly `/src/main.jsx`.
  • JSX not compiling: ensure you use Vite’s dev server (npm run dev) — it handles the transform.
  • Module not found: check relative imports (start with `./` or `/`).

Next steps

  • Add a router: react-router-dom
  • Add state management (Zustand, Redux Toolkit)
  • Add CSS modules or Tailwind CSS
  • Add unit tests (Vitest for Vite-native testing)

Conclusion

You now have a minimal, fast React app without Create React App. The total setup is lightweight and excellent for prototypes or production apps that want control over configuration. From here, incrementally add the tools you need (TypeScript, routing, testing) while keeping build times low.

Happy coding!