Project & Tooling
Create React App is deprecated
Section titled “Create React App is deprecated”For years the default answer to “how do I start a React app?” was Create React App (CRA). That era is over. react.dev is direct about it:
No. Create React App has been deprecated. For more information, see Sunsetting Create React App. — react.dev/learn/installation
And the blog post itself names what to do instead:
Today, we’re deprecating Create React App for new apps, and encouraging existing apps to migrate to a framework, or to migrate to a build tool like Vite, Parcel, or RSBuild. — react.dev/blog
Installing CRA now prints a warning pointing you to current options. Do not start new projects with it, and consider migrating existing ones.
The replacement is a fork in the road: a build tool for a client-side SPA, or a framework for anything that needs routing, server rendering, or Server Components.
Vite for a single-page app
Section titled “Vite for a single-page app”For a client-rendered SPA, Vite is the standard. Currency check first — always run this yourself before copying a tutorial’s numbers, because this exact command is what dated this lesson:
npm view vite version # 8.3.1 at the time of writingnpm view @vitejs/plugin-react version # 6.1.1npm create vite@latest my-app -- --template react-tscd my-appnpm installnpm run devReal scaffold output from that exact command, right now:
> npx> 'create-vite' my-app --template react-ts
◇ Scaffolding project in .../my-app...└ Done. Now run:
cd my-app npm install npm run devAnd the package.json it writes:
{ "scripts": { "dev": "vite", "build": "tsc -b && vite build", "lint": "oxlint", "preview": "vite preview" }, "dependencies": { "react": "^19.2.8", "react-dom": "^19.2.8" }, "devDependencies": { "@vitejs/plugin-react": "^6.1.1", "oxlint": "^1.81.0", "typescript": "~6.0.2", "vite": "^8.3.0" }}Two currency points that trip up anyone copying an older tutorial:
-
The template no longer ships ESLint. It ships oxlint instead — a Rust-based linter, configured in
.oxlintrc.json, withreact/rules-of-hookson by default. If you wanteslint-plugin-react-hooks’s compiler-aware rules (below), you add ESLint yourself; it isn’t there out of the box anymore. -
@vitejs/plugin-reactv6 changed its transform. react.dev’s own installation docs are explicit:@vitejs/plugin-reactuses Oxc. In@vitejs/[email protected], the inline Babel option was removed. — Vite’screate-viteReact template READMEBefore v6, the plugin used Babel for Fast Refresh and JSX. From v6 on, it uses Oxc for the default transform — smaller install, no Babel dependency for the common case.
@vitejs/plugin-react-swcremains a separate, third option: it swaps in SWC specifically to speed up cold start and HMR on very large projects. Pick Oxc (the default) unless you have a specific reason to reach for SWC — plainreact()with no Babel plugins is now the fast path, not the slow one.react.dev’s
build-a-react-app-from-scratchguide also mentions RSbuild (Rspack-powered) as an alternative scaffold alongside Vite — worth knowing the name exists, even though this course standardizes on Vite.
The React Compiler, for real
Section titled “The React Compiler, for real”The React 19 module’s the-react-compiler lesson shows the actual Babel transform output — read that first if you haven’t. This section proves the install wiring works, which that lesson doesn’t cover.
Because @vitejs/[email protected]+ removed the inline babel: {...} option, wiring the compiler needs a second plugin, @rolldown/plugin-babel, plus the reactCompilerPreset helper @vitejs/plugin-react v6 exports for exactly this:
npm install -D babel-plugin-react-compiler @rolldown/plugin-babel// vite.config.ts — illustrative; not type-checked here (needs @rolldown/plugin-babel + babel-plugin-react-compiler, which the shared course probe doesn't install — the real, separate build proof is in the terminal output right below)import { defineConfig } from 'vite'import react, { reactCompilerPreset } from '@vitejs/plugin-react'import babel from '@rolldown/plugin-babel'
export default defineConfig({ plugins: [ react(), babel({ presets: [reactCompilerPreset()] }), ],})This is not a config that merely parses — it was run end to end in this course’s probe against the default Vite scaffold, npm run build (tsc -b && vite build):
vite v8.3.1 building client environment for production...✓ 22 modules transformed.dist/assets/index-C9S9Cn0y.js 223.97 kB │ gzip: 69.84 kB✓ built in 252msExit 0. With build.minify: false for a readable bundle, the output literally contains the compiler’s runtime import:
$ grep -c "compiler-runtime" dist/assets/index-*.js2Two references — one for the import, one for the optimizeDeps.include entry reactCompilerPreset adds automatically (react/compiler-runtime). That’s the whole install: two dev dependencies, one extra plugin entry, and the compiler runs on your real build — no separate step, no watch mode surprises.
eslint-plugin-react-hooks v7: flat config, real errors
Section titled “eslint-plugin-react-hooks v7: flat config, real errors”Since the Vite template no longer ships ESLint, add it — and add the plugin that gives you both the classic Hooks rules and the compiler’s own diagnostics as lint errors:
npm install -D eslint @typescript-eslint/parser eslint-plugin-react-hooksimport reactHooks from 'eslint-plugin-react-hooks';import tsParser from '@typescript-eslint/parser';import { defineConfig } from 'eslint/config';
export default defineConfig([ { files: ['**/*.{ts,tsx}'], languageOptions: { parser: tsParser } }, reactHooks.configs.flat['recommended-latest'],]);recommended-latest ([email protected], the version this course’s probe has installed) turns on rule-of-hooks and the newer compiler-specific rules in one preset — no separate compiler-eslint package to add. Two real violations, run through that exact config:
import { useState } from 'react';
export function Toggle({ show }: { show: boolean }) { if (show) { const [on, setOn] = useState(false); // hook called conditionally return <button onClick={() => setOn(!on)}>{String(on)}</button>; } return null;}Bad.tsx 5:25 error React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? react-hooks/rules-of-hooksimport { useEffect, useState } from 'react';
export function Counter({ step }: { step: number }) { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => setCount((c) => c + step), 1000); return () => clearInterval(id); }, []); // missing dependency: step return <output>{count}</output>;}Counter.tsx 8:6 warning React Hook useEffect has a missing dependency: 'step'. Either include it or remove the dependency array. If 'setCount' needs the current value of 'step', you can also switch to useReducer instead of useState and read 'step' in the reducer react-hooks/exhaustive-depsBoth real, both from the exact config above — no hand-written error text.
TypeScript strict, from the start
Section titled “TypeScript strict, from the start”This course’s own probe (and every scaffold above) turns on strict plus two settings worth knowing by name, because they change what “valid TypeScript” means in this specific setup:
erasableSyntaxOnly— rejects TS-only runtime constructs (enum, constructor parameter properties) that don’t erase to nothing at the type level. Modern Node can run.tsfiles directly by stripping types; this flag keeps your code compatible with that by construction.noUnusedLocals/noUnusedParameters— an unused import or destructured prop is a compile error, not a lint warning. Generated code with a leftover unused import fails the build here, not justeslint.
The TypeScript with React lesson goes deep on typing components, hooks, and events under this same strict config.
A framework for routing, SSR, and RSC
Section titled “A framework for routing, SSR, and RSC”The moment you need real routing, server-side rendering, data loading, or React Server Components, reach for a framework instead of assembling it yourself. The current recommendations from the React docs:
npx create-next-app@latest # Next.js — App Router, RSC, SSRnpx create-react-router@latest # React Router (framework mode) — routing + SSRflowchart TB
q{"Need routing / SSR /
Server Components?"}
q -->|no, client SPA| vite["Vite
(build tool, Oxc transform)"]
q -->|yes| fw["Framework:
Next.js or React Router"]
cra["Create React App"] -->|deprecated 2025| x["do not use"] The framework owns concerns a build tool leaves to you: nested routes, data fetching on the server, streaming, and the RSC boundary (covered in the React 19 module). For a dashboard-style SPA behind a login, Vite is plenty; for a content site or anything SEO- or server-data-heavy, a framework earns its weight.
The dev experience
Section titled “The dev experience”Whatever you choose, these make React development pleasant:
- React DevTools (browser extension) — inspect the component tree, props, state, and hooks; the Profiler shows why and how often components render (essential for the performance module).
- Fast Refresh — edit a component and see the change instantly, preserving component state. Built into Vite and the frameworks.
- The modern JSX transform — you no longer need
import React from 'react'at the top of every file just to use JSX; the compiler injects the runtime import. (You still import hooks and APIs you use.) - Strict Mode — wrap your app in
<StrictMode>in development to surface impure renders and effect bugs (the double-invoke you met in Foundations).
Mental Model
Section titled “Mental Model”None of this tooling is React. React ships a rendering library and nothing else, on purpose — every choice here (Oxc vs. SWC transform, ESLint vs. oxlint, Vite vs. a framework) is a converged community answer to “what do you put around it,” not a React API. Read a currency claim in this lesson the same way: as “what’s converged as of the version numbers shown,” re-checkable with the exact npm view / scaffold commands given, not as something to memorize.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
Vite + @vitejs/plugin-react (Oxc, default) | Smaller install, no Babel dependency, fastest common path | No inline Babel plugins — the compiler needs the separate @rolldown/plugin-babel wiring shown above |
@vitejs/plugin-react-swc | Faster cold start/HMR on very large codebases | A second transform to know about; not needed unless Oxc is actually the bottleneck |
| A framework (Next.js / React Router) over Vite | Routing, SSR, RSC handled for you | More to learn, more opinionated structure, often overkill for a small SPA |
oxlint (scaffold default) vs. adding ESLint + eslint-plugin-react-hooks | oxlint is instant, zero-config, react/rules-of-hooks included | Only ESLint’s recommended-latest gives you the React Compiler’s own diagnostics as lint errors |
Common Misconceptions
Section titled “Common Misconceptions”- “CRA is old but still fine for a quick project.” react.dev’s own installation guide answers this directly: “No. Create React App has been deprecated.” It isn’t a style preference anymore — the docs actively steer you away from it.
- “Adding
@vitejs/plugin-reactgives you Babel.” Not since v6 — the default path is Oxc, with no Babel dependency at all. You only add Babel back (via@rolldown/plugin-babel) if you need a Babel-only plugin, like the React Compiler. - “The Vite React template ships ESLint.” It ships oxlint. If a generated project has an
eslint.config.jsyou didn’t add, check whether it actually runs — an empty or stale config is worse than none, because it looks like linting is happening.
Reading AI-generated code
Section titled “Reading AI-generated code”- Does the project still have a legacy
.eslintrc.*file alongside a current ESLint install? ESLint 9+ is flat-config-only by default and refuses to run at all against a.eslintrc.*with noeslint.config.*present — this isn’t a style issue, it’s a hard failure. - If
vite.config.tspassesreact({ babel: { plugins: [...] } }), check the installed@vitejs/plugin-reactversion — that option doesn’t exist from v6.0.0 on, and depending on the exact types, passing it can be a silent no-op rather than a build error. - Does a hook get called after an early
return, or inside anif?react-hooks/rules-of-hookscatches this only if it’s actually configured and running — confirm the lint command is wired into CI, not just present inpackage.json. - Is
eslint-plugin-react-hooksonrecommended(rules-of-hooks + exhaustive-deps only) when the project also wants React Compiler diagnostics (immutability,purity,set-state-in-render, …)? Those only come fromrecommended-latest.
🐛 Spot the bug
import react from '@vitejs/plugin-react'
export default {plugins: [ react({ babel: { plugins: ['babel-plugin-react-compiler'] } }),],}Show the answer
This is the pre-6.0.0 way to wire the React Compiler into @vitejs/plugin-react, copied into a project running a current version of the plugin. Checked directly against @vitejs/[email protected]’s own type definitions: Options has no babel field anymore — passing it either fails to type-check under strict TypeScript, or is silently ignored at runtime, depending on how the config is written. Either way the compiler never runs, with no error telling you so. The current wiring needs a second, separate plugin: @rolldown/plugin-babel, with the compiler passed as a preset via reactCompilerPreset() — proven with a real vite build earlier in this lesson (grep -c "compiler-runtime" dist/assets/index-*.js → 2, confirming the transform actually ran). Always check npm ls @vitejs/plugin-react before copying an install snippet — this is exactly the kind of detail that goes stale between the plugin’s major versions.