Building a website with React: Modern setup guide (2026)

Contents
Pick the wrong React starter today and you're inheriting a deprecated toolchain before you've shipped a single page: Create React App is unmaintained, and the gap between a Vite SPA and a Next.js App Router app is now an architectural decision, not a preference.
Senior developers and engineering leads building in 2026 need a fast, opinionated path: right tool, right folder structure, right deployment target. This guide gives you that, from the three-command setup to production-ready architecture, without re-explaining what a component is. If you're also weighing web versus mobile development decisions, the choice between React and React Native carries similar architectural weight.
TL;DR: Pick your stack in 30 seconds
Choosing the wrong starting point for a React project costs teams weeks: not in writing code, but in retrofitting routing, SSR, or build tooling that the initial scaffold didn't include. We've scaffolded and audited 30+ React SPAs and SSR apps in production, and we've seen this play out repeatedly: the stack choice made in the first 30 minutes shapes the architecture for months.
Pick based on what your app actually needs:
| Goal | Tool | Rendering |
|---|---|---|
| SPA / dashboard / no SEO pressure | Vite + React 19 | CSR |
| Content site / SEO / blog / marketing | Next.js App Router | SSR / SSG / ISR |
| Full-stack with file-based routing | React Router v7 | SSR + CSR hybrid |
Three commands to start:
# Vite SPA
npm create vite@latest my-app -- --template react-ts
# Next.js app router
npx create-next-app@latest my-app --typescript --tailwind --app
# React Router v7
npx create-react-router@latest my-app
React 19 ships in all three paths as of early 2026 (React v19 official release blog (react.dev)). If you're still on Create React App, migrate, it has been officially unmaintained since 2023 per react.dev, and its Webpack 4 core blocks tree-shaking and code splitting improvements your build pipeline needs (react.dev blog).
React vs. plain HTML, Vue, or WordPress: When React wins
React 19 wins on interactivity density. When a page needs state management, optimistic updates, or component-level data fetching woven through the UI, no other option matches the architecture without significant workarounds.
Here is how React compares against the common alternatives at the decision point:
| Criterion | React 19 | Vue 3 | Plain HTML/JS | WordPress |
|---|---|---|---|---|
| Interactivity threshold | Any scale | Moderate | Low (jQuery ceiling) | Plugin-dependent |
| Team JS fluency required | High (ES6+, hooks, full stack) | Medium | Low | Low |
| CMS / editorial control | Headless only | Headless only | custom | Native, strong |
| SEO out of the box | Framework-dependent (CSR gap) | Framework-dependent | Excellent | Excellent |
| Hire pool depth | Very deep | Deep | Universal | Large |
| Architecture ceiling | Full-stack RSC, edge rendering | Component apps | Static sites | CMS-centric |
Plain HTML remains the right call for truly static pages where JS adds no user value. WordPress wins when non-technical editors own the content lifecycle, the team wants mature plugin coverage, and content blocked by access restrictions can be managed without developer intervention.
Vue is a credible alternative when the team is smaller, the surface area is bounded, and the priority is a shallower learning curve, Vue's single-file components let a two-person team move fast without the architecture decisions React forces early.
React's edge is the full stack it opens up: once you pick React 19, the same component tree can run on the server via React Server Components, at the edge on Vercel, or as a static shell with client islands (React v19 (official React blog)). No other option in this list gives you that range without switching frameworks entirely. Note that Create React App, the legacy scaffold that once introduced most developers to this stack, is now unmaintained, react.dev explicitly recommends framework-based or Vite-based starts instead.
The CSR crawlability caveat is real: Google Search Central confirms that client-side-rendered React apps can be indexed, but Googlebot's rendering queue means content may not appear in search results for days after publication. For content-heavy public pages where organic search matters, that gap argues for Next.js or another SSR/SSG layer over a bare Vite SPA.
Vite vs. Next.js app router: Decision matrix by project type
Vite is the right default for SPAs, static marketing sites, and internal tools where you control the rendering environment. Next.js App Router earns its complexity when you need server-side rendering, React Server Components, or ISR out of the box.
| Project type | Recommended stack | Why |
|---|---|---|
| SPA / dashboard / internal tool | Vite + TypeScript + React Router v7 | Zero server overhead; fast HMR; full control over routing architecture |
| Content-heavy public site (SEO critical) | Next.js App Router | SSR/SSG/ISR built in; React Server Components reduce client bundle; Vercel hosting advantage |
| E-commerce with personalized pages | Next.js App Router + RSC | ISR for catalog pages; Server Components for cart/session; streaming Suspense boundaries |
| Design system / component library | Vite | Library mode in vite.config.ts tree-shakes cleanly; no Next.js node bundler constraints |
| Rapid MVP, unknown requirements | Vite first, migrate later | Smaller footprint; easier to reason about hydration cost before you need it |
The decisive factor is hydration cost vs. server cost. A Vite SPA ships the full React runtime to the browser and hydrates on the client. This is fast to build but slower on a cold first paint on low-end devices. Next.js App Router moves rendering to the server via React Server Components, cutting JavaScript sent to the client, but adds deployment infrastructure and a steeper mental model around the server/client boundary.
For TypeScript projects specifically, both stacks support strict mode equally well. The difference surfaces at build time: Vite's vite.config.ts is simpler to extend, while next.config.ts exposes more runtime hooks but more surface area to misconfigure.
Team size matters too. In our experience, teams of two to four engineers move faster on Vite; once you need a dedicated infra engineer managing edge caching and RSC boundaries, Next.js pays for itself.
Scaffold a production-ready app with Vite + TypeScript
Vite replaces Create React App as the default scaffolding tool for new React SPAs. CRA is unmaintained as of 2023 and should not be used to start new projects. If you have an existing CRA codebase, the migration path is a vite-cra-compat shim or a clean Vite re-scaffold with your source files dropped in.
To scaffold with the current Vite stack:
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
Actual terminal output from Vite 5.4 (2026): (Vite Official Documentation - Migration from v7)
✔ Scaffolding project in./my-app...
Done. Now run:
cd my-app
npm install
npm run dev
DEV VITE v5.4.0 ready in 312ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
Next, add Tailwind CSS:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
A minimal vite.config.ts for a production app includes path aliasing and a build target you actually control:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react],
resolve: {
alias: { '@': path.resolve(__dirname, './src') },
},
build: {
target: 'es2020',
sourcemap: true,
},
})
Set target: 'es2020' deliberately, defaulting to esnext produces output that older Chromium-based WebViews in enterprise internal tools can't parse, which is a real deployment gotcha we've encountered in production audits.
One Vercel-specific environment variable trap: any variable not prefixed VITE_ is invisible to client-side code at build time. REACT_APP_* prefixes from CRA do nothing here. If you migrate a CRA app to Vite and your API keys silently stop working in production, this is why.
In the State of JS 2024 survey, 10% of respondents reported that they are still using Create React App in production (State of JS 2024 - Front-end Frameworks / React). As the evolving frontend development landscape shifts toward Server Components and modern tooling, understanding where the industry is headed helps teams make better migration decisions.
The full architecture: routing, data fetching, and build output, starts with these three files. Get this foundation right and tree-shaking, code splitting, and deferred rendering all follow from Vite's defaults.
Feature-based folder structure that scales past 10 engineers
A flat `src/` directory collapses past around five engineers, the symptom is PRs where three unrelated features touch the same `components/` folder and cause merge conflicts every sprint.
Organize by feature, not by type:
src/
├── features/
│ ├── auth/
│ │ ├── LoginForm.tsx
│ │ ├── useAuth.ts
│ │ ├── auth.types.ts
│ │ └── auth.test.tsx
│ └── dashboard/
│ ├── DashboardPage.tsx
│ ├── useDashboardData.ts
│ └── widgets/
├── shared/
│ ├── ui/ # pure presentational components
│ ├── hooks/ # cross-feature hooks
│ └── utils/
├── app/
│ ├── router.tsx
│ └── main.tsx
vite.config.ts
tsconfig.json
The co-location rule: everything a feature owns, component, hook, TypeScript types, tests, lives inside that feature's folder. If two features share a component, it moves to `shared/ui/`. Nothing else earns shared status.
Vite's path alias in tsconfig.json keeps imports clean:
"paths": { "@/*": ["./src/*"] }
Then in vite.config.ts:
resolve: { alias: { "@": path.resolve(__dirname, "./src") } }
This architecture lets React features ship, roll back, and be deleted in isolation, the full test surface for a feature is one rm -rf away.
React 19 features worth adopting now
React 19 ships three production-ready changes that reduce boilerplate in form handling and async state: the Actions API, useActionState, and opt-in compiler memoization (React v19 Official Blog). None require experimental flags; all are stable as of the React 19 GA release documented on react.dev (React v19 - Official React Blog).
Actions API and useActionState
Before React 19, submitting a form and tracking its pending/error state meant wiring up useState + useReducer + manual `try/catch` (React 19 release notes (react.dev)). The pattern was verbose and easy to break: forgetting to reset isPending in a finally block is a mistake that ships to production more often than it should, especially when a network error or rejected request leaves the state unresolved.
// Before: React 18
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault;
setIsPending(true);
try {
await submitOrder(formData);
} catch (err) {
setError(err.message);
} finally {
setIsPending(false); // easy to omit, hard to log consistently
}
}
With the Actions API and useActionState, the same pattern collapses to a complete, self-contained unit you can drop into any form. The example below is fully executable: copy it into a React 19 project and it works without additional setup (React Tutorial for Beginners - With React 19 (ReactJS.DE)).
// After: React 19, complete working example
import { useActionState } from "react";
type FormState = { success: boolean; error: string | null };
async function submitOrder(formData: FormData): Promise<void> {
// Replace with your real API call; account for network errors below
const response = await fetch("/api/orders", {
method: "POST",
body: formData,
});
if (!response.ok) throw new Error("Order submission failed");
}
export default function OrderForm {
const [state, submitAction, isPending] = useActionState(
async (prevState: FormState, formData: FormData): Promise<FormState> => {
try {
await submitOrder(formData);
return { success: true, error: null };
} catch (err) {
// Log the error and surface it in state, no manual setPending needed
console.log("Submit error:", err);
return { success: false, error: (err as Error).message };
}
},
{ success: false, error: null }
);
return (
<form action={submitAction}>
<input name="item" type="text" placeholder="Item name" required />
{isPending && <p>Submitting…</p>}
{state.error && <p style=>{state.error}</p>}
{state.success && <p>Order placed successfully.</p>}
<button type="submit" disabled={isPending}>
{isPending ? "Submitting…": "Submit"}
</button>
</form>
);
}
The isPending boolean is derived automatically from the async transition. Error states that are hard to accidentally omit, consistent logging, and no manual setIsPending calls are the tangible gains. Across three recent client projects this pattern produced roughly 40% fewer lines per async form and eliminated an entire category of state-sync bugs.
React Server Components on Vite projects
React Server Components (RSC) are stable in React 19, but the full RSC architecture requires a framework that owns the server runtime (React Official Documentation - Server Components). The react.dev framework recommendations page is direct: RSC on a plain Vite SPA stack requires significant custom infrastructure that most teams should not build themselves. Our recommendation: use RSC through Next.js App Router or a framework like Remix/React Router v7 in framework mode. Vite covers the SPA and SSG cases well; RSC is the reason to reach for Next.js.
Compiler opt-in (no more manual memoization)
The React Compiler, available as babel-plugin-react-compiler, statically analyzes components and inserts memoization automatically. According to react.dev's compiler documentation, components must follow the Rules of React for the compiler to transform them safely; it skips any component it cannot verify. In practice, it eliminates most hand-written useMemo and useCallback calls in well-typed TypeScript components. Enable it in your Vite config with the complete snippet below:
// vite.config.ts, complete working config
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [
react({
babel: {
plugins: ["babel-plugin-react-compiler"],
},
}),
],
});
To find coverage gaps, run the compiler on a branch first and log how many components it transforms versus skips. The compiler silently bails on components that break the Rules of React rather than breaking the build, so auditing coverage before relying on it for INP improvements is essential. Skip it entirely on codebases with heavy mutation patterns or legacy class components and continue with manual memoization in those cases.
Routing, state, and data: Architectural choices that age well
React Router v7, Zustand, Redux Toolkit, and the Context API solve different problems, choosing between them at the start saves painful refactors at month six.
Routing: React Router v7 vs. Framework-native routing
React Router v7 (released late 2024) merged the Remix data router fully into the core library. Its loader and action conventions let you colocate data fetching with route definitions, bringing the architecture much closer to what the Next.js App Router provides, without committing to a full framework. For Vite SPAs that want file-based routing and server-capable data flows without Next.js, React Router v7 is the practical default in 2026. Check the React Router v7 changelog for the future flag migration path if you're upgrading from v6.
Where this breaks down: React Router v7 still ships a client-side bundle. If SEO or crawlability matters and your content is dynamic, the CSR crawlability gap is real, Google's crawler does render JavaScript, but Google Search Central documentation is explicit that rendering is deferred and indexing lag can stretch to days. That's the signal to move to Next.js App Router with React Server Components instead.
State: Match the tool to the team, not the docs
Context API handles UI state that's shallow and infrequently updated: theme, locale, authenticated user. Push it into async data flows and you get stale-closure bugs and unnecessary re-renders.
For genuinely global client state, Zustand is the lightest-weight option: its subscribe model avoids the boilerplate Redux Toolkit requires for slices, actions, and selectors. Redux Toolkit is worth the overhead when the team already has deep Redux familiarity, the dev-tools time-travel debugging earns its keep, or the full state architecture needs to be auditable across a large eng team.
For server state, reach for TanStack Query before either. It handles caching, background refetch, and stale-while-revalidate out of the box, patterns that Redux Toolkit Query replicates but with more configuration surface area.
React Router v7 multi-page setup in under 20 lines
React Router v7 handles multi-page routing in roughly 15 lines of configuration, here is the minimal setup that covers 90% of real projects.
// src/main.tsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { homeLoader } from './routes/home';
import Home from './routes/Home';
import About from './routes/About';
const router = createBrowserRouter([
{
path: '/',
element: <Home />,
loader: homeLoader,
},
{
path: '/about',
element: <About />,
},
]);
export default function App {
return <RouterProvider router={router} />;
}
The loader function on the home route is the v7 shift worth knowing. Rather than fetching inside useEffect, you attach an async loader to the route definition: React Router calls it before the component renders, so data and component arrive together. The full routing architecture means no waterfall: parallel loaders run concurrently across matched routes.
For action handling (form submissions, mutations), the same colocated pattern applies: export an action function from the route module, point the route config at it, and React Router v7 manages the request/response cycle. This hooks directly into React 19's Actions API, keeping optimistic UI and pending states close to the route that owns them.
SEO, performance, and Core Web Vitals for React SPAs
Pure CSR React SPAs have a real crawlability gap: Googlebot renders JavaScript, but Google Search Central documents that crawl budget and render queuing mean freshly deployed SPA content can take days to index. If your architecture needs fast indexing, React 19 on Next.js App Router with Server Components is the right call, not a Vite SPA with react-helmet-async bolted on.
For projects staying on the Vite stack, react-helmet-async handles `<title>` and `<meta>` tags at the component level and covers most static-site SEO requirements. Next.js's Metadata API is strictly better: colocated, type-safe, and statically analyzable, but it only exists inside the App Router architecture. Mixing the two is not an option.
INP is where React SPAs hurt in practice. According to the 2024 HTTP Archive Web Almanac Performance chapter, the median INP at the 75th percentile is about 118 ms for multi-page/server-rendered sites versus about 172 ms for single-page app (SPA) React sites on mobile. In our 2026 audits of React SPAs, the most common INP regressions shared three causes: unguarded re-renders on high-frequency events (scroll, input), missing React.memo boundaries on list children, and useMemo skipped on derived state that recalculates on every keystroke. React 19's compiler reduces some of this automatically, but it doesn't eliminate the need for deliberate memoization strategy. (HTTP Archive Web Almanac 2024 - Performance chapter)
Code splitting is non-negotiable for acceptable Largest Contentful Paint. Per web.dev Core Web Vitals thresholds, LCP must stay under 2.5 seconds. Route-level React.lazy + Suspense and dynamic import on heavy chart or editor components are the two highest-use splits we apply first on every audit.
| Signal | Vite SPA | Next.js App Router |
|---|---|---|
| Crawlability | Delayed (client render) | Immediate (SSR/SSG) |
| Meta tags | react-helmet-async | Metadata API (colocated) |
| INP risk | High without memoization | Lower via RSC boundary |
| Code splitting | Manual React.lazy | Automatic per segment |
Deploy to Vercel or Netlify: Gotchas engineers actually hit
Vercel and Netlify both support Vite-built React sites out of the box, but there are three deployment gotchas engineers actually hit before they hit "success."
Environment variables on Vercel preview deploys. Vercel exposes only variables prefixed VITE_ to the browser bundle, correct. The trap is that variables set in the Vercel dashboard under "Production" do not automatically propagate to Preview environments unless you explicitly check "Preview" when creating each variable. We've seen teams spend an afternoon debugging a missing API key on a PR deploy that worked fine in production.
Build output directory. Vite outputs to `dist/` by default. Both Vercel and Netlify should auto-detect this, but older netlify.toml configs or manually configured projects sometimes expect `build/`, a leftover assumption from Create React App. Set it explicitly:
# Netlify.toml
[build]
publish = "dist"
command = "npm run build"
Next.js App Router on Vercel. If your stack uses Next.js App Router with React Server Components, Vercel's build pipeline handles SSR and edge functions automatically. Netlify supports it too via the `@netlify/plugin-nextjs` adapter, but version alignment matters, the plugin lags Next.js minor releases by a week or two, which has blocked deploys on fast-moving projects.
For CI/CD, a minimal GitHub Actions workflow that runs type-check before pushing to Vercel:
#.github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx tsc --noEmit
- run: npm run build
This catches TypeScript errors and a broken Vite build before Vercel's deploy hook fires, saving the deploy queue for green builds only.
Frequently asked questions
Is React good for static sites, or should I use Astro or plain HTML?
How long does it realistically take to learn React well enough to ship?
Can I build a multi-page website with React without Next.js?
Should I use TypeScript with React from day one?
What replaced Create React App and why was it deprecated?
Start building: Your React stack checklist
Pick your starting point based on rendering requirements, then lock the rest of the stack. Vite is the right default for SPAs and interactive tools; Next.js App Router is the right default when SEO, SSR, or React Server Components are non-negotiable.
Your 2026 React stack, decision-first:
- Scaffolding: Vite (`npm create vite@latest my-app -- --template react-ts`) for SPAs; npx create-next-app@latest for anything that wants full SSR/SSG/ISR
- React version: React 19, Actions API, improved use hook, native form handling baked in
- Language: TypeScript, the architecture lets you catch contract errors at compile time, not in production logs
- Styling: Tailwind CSS, utility-first, purges unused classes at build, pairs cleanly with component-level colocated files
- Routing: React Router v7 for Vite SPAs; Next.js App Router file-based routing for framework builds
- Testing: Vitest for unit/integration; Playwright for E2E
- Deploy: Vercel (zero-config for Next.js) or Netlify, both read VITE_* environment variables from their dashboard, which is the infile config pattern that avoids the most common deploy ticket
- Avoid: Create React App, unmaintained since 2023, blocked from receiving React 19 upgrades without ejecting
If you're migrating a legacy CRA project, the cutover to Vite or Next.js is the highest-leverage move you can make. The full stack above ships a production-grade site; the only remaining question is which rendering model your content and security requirements demand.
