Blog

Part 10 · Your First React Project on Your Own Computer, with Vite

Make a real React project on your computer with Vite. Learn what each file does, how the page updates when you save, how to build for real users, and the first mistakes people make.

So far, every example in this series ran here, in the page. That is a good way to learn. But real apps live on your own computer, in a folder of files. In Part 1 and Part 2 we said a tool called Vite does the work there. This part sets up that tool.

We’ll make a new project, look at every file in it, and run it. Then we’ll change a file and watch the page update. We’ll also see what the real project checks that the playground doesn’t. At the end, we’ll build the app for real users.

We ran every command in this part on our own computer, and copied what it printed.

Try this first

Run this small app in the page. It has two components, Greeting and Counter. Counter prints a line each time it renders.

import { useState } from 'react'

function Greeting({ name }: { name: string }) {
  return <p>Hello, {name}!</p>
}

function Counter() {
  const [count, setCount] = useState(0)
  console.log('Counter renders, count =', count)
  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  )
}

export default function App() {
  return (
    <div>
      <Greeting name="Ana" />
      <Counter />
    </div>
  )
}
Counter renders, count = 0
Counter renders, count = 0
Counter renders, count = 1
Counter renders, count = 1

Press Run, then click the button once. The Console shows four lines. Each line is there twice, because the playground uses Strict Mode, like a new project does. Part 2 explained why.

Later in this part, we’ll move this same code into a real project. Then we’ll build it for real users. Make a guess. In that finished app, you load the page and click once. How many lines will the console show?

What you need

You need three things on your computer.

Node.js. Node.js is a program that runs JavaScript outside the browser. Vite is written in JavaScript, so it needs Node.js to run. Node.js comes with npm, a tool that downloads code other people wrote. A downloaded piece of code is called a package.

Vite’s own guide says: “Vite requires Node.js version 20.19+, 22.12+.” So you need version 20.19 or newer, or version 22.12 or newer. On the download page at nodejs.org, pick the version marked LTS. The Node.js project says its LTS versions “are ready for general use”. We used Node.js v24.18.0 and npm 11.16.0.

An editor. This is the program where you write code. React’s docs call VS Code “one of the most popular editors in use today”. It is free. It also shows TypeScript errors while you type, which the playground can’t do.

A terminal. A terminal is a window where you type commands instead of clicking. On a Mac, open the app called Terminal. On Linux, it is with your other apps. On Windows, there are two apps: PowerShell and Command Prompt. PowerShell has a setting that can stop it from running scripts. Microsoft’s docs call it the “execution policy”. If PowerShell refuses to run npm and its message talks about running scripts, use Command Prompt instead.

In the terminal you type a command and press Enter. The terminal is always “in” one folder. The command cd my-app moves it into the folder my-app. cd is short for “change directory”. A directory is a folder.

To check that Node.js works, open a new terminal after you install it. A terminal that was open before may not find it yet. Then type this:

node -v

It prints the version, like v24.18.0. If it says the command is not found, Node.js is not installed yet.

Make the project

Go to the folder where you keep your projects. Then type:

npm create vite@latest

npm downloads the newest version of the tool create-vite and runs it. Ours was version 9.2.1. The first time, npm asks before it downloads:

Need to install the following packages:
create-vite@9.2.1
Ok to proceed? (y)

Type y and press Enter. Then create-vite asks five questions. These were our answers:

◇  Project name:
│  my-app
◇  Select a framework:
│  React
◇  Select a variant:
│  TypeScript
◇  Which linter to use?
│  Oxlint
◇  Install with npm and start now?
│  No
  • Project name is the name of the new folder. It shows vite-project until you type. We typed my-app and pressed Enter.
  • The second question asks which library you want. Vite works with many. React is third in the list, so press the Down arrow twice, then Enter.
  • The third asks which kind of React project. We picked plain TypeScript, the first choice, so just press Enter.
  • The fourth asks for a linter. A linter is a tool that reads your code and warns you about likely mistakes. We kept the first choice, Oxlint, with Enter. The other choice is ESLint.
  • The last asks whether to install and start now. Yes is already picked. To follow this part step by step, press the Right arrow to pick No, then Enter.

If you press Enter on Yes, that is fine too. We tried it. create-vite ran npm install and then npm run dev for you, and stopped at the same “ready” lines you’ll see below. Then skip ahead to Run it.

You can also answer all the questions in one command:

npm create vite@latest my-app -- --template react-ts --no-interactive

react-ts is the name of the template: the starter project that create-vite copies for React with TypeScript. The -- passes the rest of the line on to create-vite. Vite’s guide shows the same pattern. We made a project both ways, and the files were exactly the same.

At the end, create-vite prints the next three steps:

└  Done. Now run:

  cd my-app
  npm install
  npm run dev

Install the packages: npm install

First move into the new folder. Then install:

cd my-app
npm install

The project needs React, Vite, TypeScript and a few more packages. npm install downloads them into a new folder called node_modules. Ours printed:

added 27 packages, and audited 28 packages in 2s

9 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

The time will be different for you, and the numbers can be different on another system. The node_modules folder was 87 MB on our computer. You never change anything inside it.

npm also writes a file called package-lock.json. It records the exact version of every package it installed. These are the main ones we got: react@19.3.0, react-dom@19.3.0, vite@8.3.3 and typescript@6.0.3. React 19.3.0 is the same version the playground uses. TypeScript is a different story: this series checks its examples with TypeScript 7.0.2, but the project gets 6.0.3. We explain why below, at package.json.

Run it: npm run dev

Now start the project:

npm run dev

The terminal shows this:

> my-app@0.0.0 dev
> vite


  VITE v8.3.3  ready in 111 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

This is the dev server. “Dev” is short for development: the time when you write and test your app. A server is a program that sends web pages to a browser. This one runs on your own computer while you work.

Open http://localhost:5173/ in your browser. You see Vite’s starter page, with a button that says “Count is 0”.

  • localhost means “this same computer”.
  • 5173 is a port. A port is a number that tells the computer which program should answer. Vite uses 5173 unless something else already has it.

The dev server keeps running, so the terminal doesn’t take new commands. To stop it, click in the terminal and hold down the Ctrl key while you press C. On a Mac, the key is marked control. Apple’s guide calls this “Control-C”. We checked: after that, the server was no longer running.

Every file in the new project

This is the whole project after npm install:

my-app/
  .gitignore
  .oxlintrc.json
  README.md
  index.html
  node_modules/          (made by npm install)
  package-lock.json      (made by npm install)
  package.json
  public/
    favicon.svg
    icons.svg
  src/
    App.css
    App.tsx
    assets/
      hero.png
      react.svg
      vite.svg
    index.css
    main.tsx
  tsconfig.app.json
  tsconfig.json
  tsconfig.node.json
  vite.config.ts

Files whose names start with a dot, like .gitignore, are hidden in many places. The ls command didn’t list them until we added -A. Your file window may hide them too.

Let’s go through them in the order the browser meets them.

index.html: the page

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>my-app</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

This is the one HTML page of your app. Vite’s guide says “index.html is the entry point to your application”. The entry point is where everything starts. Two lines matter:

  • <div id="root"></div> is an empty box. React will draw your whole app inside it.
  • <script type="module" src="/src/main.tsx"> loads your code. type="module" tells the browser the file is a JavaScript module, so it can use import and export. The browser can’t read TypeScript or JSX. So when the browser asks for main.tsx, Vite turns it into plain JavaScript first.

The dev server also adds two scripts of its own to this page before it sends it: /@vite/client and /@react-refresh. They keep a connection open to the dev server, so the page can update when you save.

src/main.tsx: where React starts

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

This short file connects React to the page. Line by line:

  1. It brings in StrictMode from React.
  2. It brings in createRoot from react-dom/client. This is the part of React that changes a real browser page.
  3. import './index.css' adds the styles in index.css to the page.
  4. import App from './App.tsx' brings in your App component from the file next to it.
  5. document.getElementById('root') finds the empty <div id="root"> in index.html.
  6. createRoot(...) tells React that this box is its own. React’s docs say createRoot lets you “display React components inside a browser DOM node”. A DOM node is one thing on the page, like that <div>.
  7. .render(...) shows <App /> inside the box.

The ! after getElementById('root') is for TypeScript. getElementById gives back null when no element has that id. The ! tells TypeScript: “trust me, it is there”. It is, because index.html has it.

<StrictMode> around <App /> turns on React’s extra checks. That is why logs appear twice while you develop. Part 2 explained it. You will rarely change this file.

src/App.tsx: your first component

This is the starter page you saw in the browser. It is 122 lines long, and it begins like this:

import { useState } from 'react'
import heroImg from './assets/hero.png'
import reactLogo from './assets/react.svg'
import viteLogo from './assets/vite.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)

You know most of this already. useState is from Part 4. Further down, the button uses an updater function: setCount((count) => count + 1). Part 4 explained those too.

Two things are new. A file can import a picture, like import reactLogo from './assets/react.svg'. Then reactLogo holds the picture’s address, ready for <img src={reactLogo} />. A file can also import a CSS file, like import './App.css'. Vite’s docs say it adds that file’s styles to the page in a <style> tag. So these styles apply to the whole page, not just to App.

The last line is export default App. It lets main.tsx import App. We’ll come back to export soon.

src/index.css and src/App.css: styles

index.css (111 lines) holds styles for the whole page, like the text size and colours. App.css (184 lines) holds the styles for the starter page. You can delete everything in both and write your own.

src/assets/ and public/: pictures and icons

There are two places for files like pictures.

  • src/assets/ holds files that your code imports. Vite processes them, as you saw with reactLogo.
  • public/ holds files that your code never imports. Vite’s docs say they are sent at the address / while you develop. When you build, they are copied as they are. So public/favicon.svg is at the address /favicon.svg. That is the small icon next to the page title, and index.html points to it.

vite.config.ts: Vite’s settings

import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
})

A plugin is a piece of code that adds a feature to a tool. This file adds one plugin, @vitejs/plugin-react. It adds React support to Vite. Vite’s docs list it for React’s Fast Refresh. That is how the page updates when you save, as you’ll see below.

tsconfig.json, tsconfig.app.json, tsconfig.node.json: TypeScript’s settings

There are three files, because two kinds of code live in this project. tsconfig.json only points to the other two.

  • tsconfig.app.json is for your app: everything in src.
  • tsconfig.node.json is for vite.config.ts, which runs in Node.js, not in the browser.

Two lines in tsconfig.app.json are worth knowing:

"noEmit": true,
"jsx": "react-jsx",

"jsx": "react-jsx" is the JSX setting from Part 1. It says JSX turns into calls to _jsx from react/jsx-runtime. Vite’s docs list jsx as one of the settings in this file that Vite follows. While you develop, Vite uses the development call. We asked the dev server for App.tsx. It sent back JavaScript, and the <h1> had become this:

_jsxDEV("h1", { children: "Get started" }

That is the _jsxDEV call from Part 1, the one used while you develop.

"noEmit": true means TypeScript only checks your code. It doesn’t write any JavaScript files. Vite does that job, so the _jsxDEV call above came from Vite, not from TypeScript.

package.json: the project’s list

{
  "name": "my-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "oxlint",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.2.8",
    "react-dom": "^19.2.8"
  },
  "devDependencies": {
    "@types/node": "^24.13.3",
    "@types/react": "^19.2.18",
    "@types/react-dom": "^19.2.7",
    "@vitejs/plugin-react": "^6.1.1",
    "oxlint": "^1.81.0",
    "typescript": "~6.0.2",
    "vite": "^8.3.0"
  }
}

scripts lists the project’s commands. npm run dev runs the dev script, which is vite. These are the four:

Command What it runs What it does
npm run dev vite Starts the dev server.
npm run build tsc -b && vite build Checks the types, then builds the finished app.
npm run lint oxlint Runs the linter.
npm run preview vite preview Shows the finished app on your computer.

dependencies are the packages your app needs in the browser: React. devDependencies are tools you only need while you work: Vite, TypeScript, the linter, and the types for React.

The ^ in "^19.2.8" means “this version, or a newer one with the same first number”. That is why npm gave us React 19.3.0. The ~ in "~6.0.2" is stricter: only 6.0.something. So this project stays on TypeScript 6, and npm gave us 6.0.3. We didn’t compare TypeScript 6 and 7 for this part. So an error message in your project may use other words than this series.

The other files

.oxlintrc.json holds the linter’s settings. It names two rules. One, rules-of-hooks, checks the rules of hooks from Part 4. We tested it with useState inside an if, and npm run lint printed an error: React Hook "useState" is called conditionally. The other rule comes up later in this part. Oxlint also runs some rules that the file doesn’t name. One of them checked an Effect in our test. Part 11 explains Effects. .gitignore lists files that Git should not save. Git is a tool that keeps the history of your code. README.md is a short note about the template.

Change a file and watch the page

Keep npm run dev running and the page open. In src/App.tsx, find <h1>Get started</h1>. Change it to <h1>Hello from my app</h1> and save the file.

The heading changes in the browser at once. You didn’t reload the page, which means loading the whole page again. This is hot module replacement, or HMR. Each file of your code is a module. HMR means Vite replaces just the one module you changed, while the page keeps running.

Here is what happened inside, step by step.

1. You change App.tsx in your editor and save it. 2. Vite sees the change. The terminal shows one line. 3. Vite sends the open page a message (only part shown). 4. The page loads the new App.tsx from Vite. 5. React shows the new App. The page did not reload. <h1>Hello from my app</h1> [vite] (client) hmr update /src/App.tsx { "type": "update", "path": "/src/App.tsx" } /src/App.tsx?t=1791358393863 Hello from my appthe heading changed, and the page did not reload

What happens when you save a component file. Press play, or step through it.

  1. You save App.tsx.
  2. Vite is watching your files. It sees the change, and its terminal prints a line: [vite] (client) hmr update /src/App.tsx.
  3. The page keeps a connection open to the dev server. Vite sends a short message over it. The figure shows only part of it. The real one had a list of updates, with "type":"js-update" and "path":"/src/App.tsx".
  4. The page loads the new App.tsx from the dev server. It adds ?t= and a number to the address, so it gets the new copy.
  5. React shows the new App. The page did not reload.

We measured steps 2 and 3 by listening on that connection while we changed files. Step 4 comes from Vite’s own page code, which loads the changed file again. We also watched step 5 in a real browser, without a screen, with the three-file app from later in this part. The button said “Clicked 3 times”. We changed its text in the file and saved. The button then said “Pressed 3 times”. A value we had stored on the page was still there, so the page had not reloaded. The count stayed 3, so the component kept its state too.

Not every change works this way. We changed four files, one after another, and Vite did different things:

File we changed Terminal line Message to the page
src/App.tsx hmr update /src/App.tsx update
src/index.css hmr update /src/index.css update
src/main.tsx page reload src/main.tsx full-reload
index.html page reload index.html full-reload

A change to main.tsx or index.html reloads the whole page. Those files start everything, so nothing else can take in the new copy.

An everyday example

Think of a book with loose pages in a folder. When one page has a mistake, you replace only that page. You don’t print the whole book again. HMR replaces one module. A full reload prints the whole book.

The exact version

Vite’s docs say HMR can update the page “without reloading the page or blowing away application state”. In plain words: the page stays, and so does what your components remember. That is their state, from Part 4. But for React, this needs one rule: a file should export only components. The linter warns you when a file breaks that rule, as you’ll see below.

Type errors: the editor sees them, the dev server doesn’t

The playground in this series doesn’t check types. Part 1 showed this. Does the real project check them?

We added one wrong line to App in App.tsx, and used title in the heading:

const title: number = 'My app'

title should hold a number, but it holds text. An editor that checks TypeScript, like VS Code, marks this line while you type.

But the dev server did nothing about it. Its terminal printed only hmr update /src/App.tsx, the same as before, and the browser got the new code. The page would show “My app” as if all was fine. Vite’s docs say why. Vite turns TypeScript into JavaScript, but it does “NOT perform type checking”. It leaves that job to your editor and to the build. That keeps it fast.

So where do you see the error? In your editor, and here:

npx tsc -b

npx runs a program from the project’s node_modules. Here it runs tsc, the TypeScript checker. It printed:

src/App.tsx(8,9): error TS2322: Type 'string' is not assignable to type 'number'.

(8,9) means line 8, character 9. npm run build also stops with the same error, because its script starts with tsc -b. It stopped before Vite built anything. If dist is there from an earlier build, it keeps the old files. We tried that too: after a failed build, dist still held the files from the build before.

Move a playground example into the project

Now let’s take the code from “Try this first” and put it in the project. We could copy it all into App.tsx, and it would work. But real projects put each component in its own file. Files stay short, and you can find things.

Make two new files in src, next to App.tsx.

src/Greeting.tsx:

export function Greeting({ name }: { name: string }) {
  return <p>Hello, {name}!</p>
}

src/Counter.tsx:

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)
  console.log('Counter renders, count =', count)
  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  )
}
Counter renders, count = 0
Counter renders, count = 0

This file is a whole small app by itself, so you can press Run here too.

Then replace everything in src/App.tsx with this:

import Counter from './Counter'
import { Greeting } from './Greeting'

function App() {
  return (
    <div>
      <Greeting name="Ana" />
      <Counter />
    </div>
  )
}

export default App

Save, and the page shows “Hello, Ana!” and the button. We checked all three files together with npx tsc -b, and it found no errors. npm run lint found no problems either.

export and import

A file shares something with other files by exporting it. Another file uses it by importing it. There are two kinds of export.

  • A default export: export default function Counter(). A file can have only one. You import it with no curly braces: import Counter from './Counter'.
  • A named export: export function Greeting(). A file can have many. You import them by their exact names, in curly braces: import { Greeting } from './Greeting'.

React’s docs say a file “can have as many named exports as you like”, but only one default export.

App.tsx shows another way to write a default export. It defines function App() first, and then writes export default App on the last line. Both ways do the same thing.

The path './Counter' means “the file Counter in this same folder”. You can leave out .tsx. The template’s main.tsx writes './App.tsx' with it. Both work.

Mixing up the two kinds is a very common mistake. We tried both wrong ways, and npx tsc -b caught each one:

src/App.tsx(1,10): error TS2614: Module '"./Counter"' has no exported member 'Counter'. Did you mean to use 'import Counter from "./Counter"' instead?

That was import { Counter } for a default export. The other mistake, import Greeting for a named export, gave error TS2613. It says the module “has no default export” and suggests import { Greeting }. The message shows the whole path to the file on your computer.

The dev server doesn’t check types, so what does the browser do? We opened the page with import Greeting in a browser without a screen. The page was blank: nothing at all inside <div id="root">. The browser reported this error:

The requested module '/src/Greeting.tsx' does not provide an export named 'default'

Vite’s terminal showed the same error, after [Unhandled error] SyntaxError:. So a blank page often means an error. Look in the browser’s console and in the terminal.

One more reason for one component per file

We added a small helper function to Greeting.tsx, next to the component:

export function shout(text: string) {
  return text.toUpperCase()
}

npm run lint then printed this warning:

src/Greeting.tsx:5:17: warning react(only-export-components): Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components.

Fast Refresh is React’s kind of HMR. As the warning says, it only works when a file exports only components. So put helpers like shout in their own file. A constant is allowed, though. The template’s settings say so (allowConstantExport), and export const greetingWord = 'Hello' gave no warning.

Build the finished app: npm run build

The dev server is for you, while you work. Your users get a build: a few plain files that any web server can send. Stop the dev server with Ctrl+C. Then type:

npm run build
> my-app@0.0.0 build
> tsc -b && vite build

vite v8.3.3 building client environment for production...
transforming...
✓ 18 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html                   0.45 kB │ gzip:  0.29 kB
dist/assets/index-BIAv5uEs.css    1.78 kB │ gzip:  0.81 kB
dist/assets/index-CAcS1TWY.js   219.90 kB │ gzip: 68.70 kB

✓ built in 99ms

Production means the real app that your users use. The build is in a new folder, dist:

dist/
  index.html
  favicon.svg
  icons.svg
  assets/
    index-BIAv5uEs.css
    index-CAcS1TWY.js

All your components, plus React itself, are now in one JavaScript file. All the CSS is in one CSS file. favicon.svg and icons.svg were copied from public/ as they were. The gzip column is Vite’s guess of each file’s size after gzip, a common way to pack files smaller.

Vite adds letters to the file names, like CAcS1TWY. They depend on what is inside. We changed one word in Counter.tsx and built again. The JavaScript file was then called index-CzFYHnxh.js. Browsers keep copies of files they have downloaded. MDN’s guide to caching explains the trick. Put a version or a “hash value” in a file’s address. Then browsers can keep the file for a long time. These letters are that hash: a short code made from the file’s contents. A new build gets new names, so browsers fetch the new files.

What changes in production

Remember the guess from “Try this first”? We ran the built JavaScript file in jsdom, a tool that acts like a browser, and clicked the button once. It logged:

Counter renders, count = 0
Counter renders, count = 1

One line per render, not two. The production build uses React’s production version, and Strict Mode’s checks don’t run there. React’s docs say Strict Mode’s checks “are development-only and do not impact the production build”. React’s warnings are left out too. We searched both builds for the text of React’s warning about list keys, from Part 8. It was in the development build, but not in the production build.

To compare, we also built the same app with React’s development version. That file logged every line twice, the same as the playground. It was also much bigger: 427.79 kB, against 219.90 kB.

The build doesn’t use _jsxDEV. We searched the built file, and jsxDEV was not in it. But you won’t find _jsx either. The build is minified: Vite makes it smaller by giving things very short names. This is our Counter button inside the built file:

(0,f.jsxs)(`button`,{onClick:()=>t(e+1),children:[`Clicked `,e,` times`]})

f.jsxs is jsxs from react/jsx-runtime, the call for several children from Part 1. f is the short name the build gave that import.

Look at the build: npm run preview

npm run preview
> my-app@0.0.0 preview
> vite preview

  ➜  Local:   http://localhost:4173/
  ➜  Network: use --host to expose

This server sends the files in dist, on port 4173. Open the address and check that your app works. Vite’s docs say vite preview is for checking the build on your own computer. It is “not meant as a production server”. To put your app online, you copy the dist folder to a real web server.

Common mistakes

Running a command in the wrong folder

You made my-app, but you forgot cd my-app. Then you type npm run dev in the folder above it. On Linux, the output starts like this:

npm error code ENOENT
npm error syscall open
npm error path …/package.json
npm error errno -2
npm error enoent Could not read package.json: Error: ENOENT: no such file or directory, open '…/package.json'
npm error enoent This is related to npm not being able to find a file.

We cut the start of each path and wrote … instead, and left out the last three lines.

npm looks for package.json in the folder you are in, and there isn’t one. The fix: cd my-app, then run the command again.

Forgetting npm install

You made the project, but you didn’t run npm install. Then you typed npm run dev:

> my-app@0.0.0 dev
> vite

sh: 1: vite: not found

We ran this on Linux. The words may differ on your computer. Vite lives in node_modules, and that folder isn’t there yet. The fix: run npm install once, then npm run dev. Run npm install also when you get a project from someone else. People don’t share node_modules.

A second dev server

You started npm run dev in one terminal, forgot about it, and started it again in another:

Port 5173 is in use, trying another one...

  VITE v8.3.3  ready in 103 ms

  ➜  Local:   http://localhost:5174/

This isn’t an error. Vite picked the next port, 5174. But now two servers run, and it is easy to look at the wrong one. Stop the one you don’t need with Ctrl+C.

If you want Vite to stop instead of picking a new port, use npm run dev -- --strictPort. Then it fails with Error: Port 5173 is already in use.

Editing files in dist

dist is made by npm run build. We changed the title in dist/index.html by hand. Then we ran npm run build again, and the title was back to my-app. The build makes dist again from your source files every time. Change the files in src, public and index.html, then build.

Practice

You need your own project for these. Make it the way this part shows.

  1. In index.html, change <title>my-app</title> to <title>My first app</title>. Save. What title does the browser show for the page now?
  2. Move the “Try this first” code into the project, as three files. Open the browser’s developer tools and look at the Console. Reload the page, then click the button once. How many lines start with Counter renders?
  3. In Counter.tsx, change setCount(count + 1) to setCount('one'). Does the page still show? What does npx tsc -b say?
  4. Run npm run build, then npm run preview, and open the address it shows. Click the button once. How many lines does the console show now?
Answers
  1. The browser shows the title “My first app”. The title comes from index.html, not from React. We tried it: Vite printed page reload index.html and reloaded the whole page.
  2. Four: Counter renders, count = 0 twice, then Counter renders, count = 1 twice. We checked in a real browser, without a screen. The <StrictMode> tag in main.tsx and React’s development version make each render run twice, the same as in the playground. The console also shows other lines: [vite] connecting..., [vite] connected., and a note from React suggesting React DevTools. React DevTools is a browser add-on. If you have it, React’s docs say the second log of each pair looks a little lighter.
  3. The page still shows, because the dev server doesn’t check types. npx tsc -b prints error TS2345 for src/Counter.tsx, line 7. It says the argument of type 'string' can’t go where TypeScript expects SetStateAction<number>. count started as a number, so setCount only takes a number. npm run build would stop at this error too. Put setCount(count + 1) back.
  4. Two: Counter renders, count = 0 once, then Counter renders, count = 1 once. We measured this by running the built file in jsdom. The build uses React’s production version, where Strict Mode doesn’t run anything twice.

Interview questions

Try to answer each one out loud before you open the answer.

What does Vite do in a React project?

It has two jobs. While you work, it runs a dev server. The server turns your TypeScript and JSX into JavaScript when the browser asks for each file. It also updates the page when you save, with hot module replacement. For users, vite build makes a small set of plain files in dist. React’s docs list Vite as one of the build tools for starting a React app.

A strong answer adds that Vite doesn’t check types. That is the job of the editor and of tsc. The template’s build script runs tsc first.

What is the difference between npm run dev and npm run build?

npm run dev starts a server for development. It sends React’s development version. Together with the <StrictMode> tag in main.tsx, that gives double renders and all of React’s warnings. It doesn’t check types. npm run build first runs tsc -b, so a type error stops it. Then it writes the finished app into dist, with React’s production version. There are no double renders and no development warnings there, and the files are smaller.

A strong answer mentions npm run preview, to look at the build on your own computer. It also says vite preview is not meant to be a real production server.

What happens in the file that starts a Vite React app?

That file is src/main.tsx. It finds the <div id="root"> in index.html. It passes the div to createRoot, so React owns everything inside it. Then it calls render with <App /> inside <StrictMode>. It also imports the global CSS file. You rarely change it. In our test, changing it made Vite reload the whole page, not replace one module.

Why does the template turn on Strict Mode? Does it slow down production?

Strict Mode runs extra checks while you develop. It calls components twice to find code that isn’t pure. Pure code gives the same result every time and changes nothing outside itself. It also runs Effects an extra time, which Part 11 covers. These checks find bugs early. They don’t affect production: React’s docs say they “are development-only and do not impact the production build”. We checked: a built app logged once per render, not twice.

A strong answer adds the rest of the list from React’s docs. Strict Mode also runs ref callbacks an extra time. It calls twice the functions you pass to useState, set functions, useMemo and useReducer.

The dev server shows the page, but npm run build fails. Why?

Usually a type error. The dev server only turns TypeScript into JavaScript and never checks types. The build script runs tsc -b first, and tsc stops on any type error. The fix is to read the error, which names the file and line, and fix the code. Turning the check off is the wrong fix.

What is the difference between a default export and a named export?

A file can have one default export and any number of named exports. You import a default export with any name you like, and no curly braces: import Counter from './Counter'. You import a named export by its exact name, in curly braces: import { Greeting } from './Greeting'. Mixing them up is a type error: “has no default export”, or “has no exported member”.

A strong answer adds that a default import can take any name. So one component can have different names in different files. A named import must use the exported name.

What is hot module replacement?

When you save a file, the dev server sends the open page a message. The page loads only the changed module and puts it in place, without a reload. Vite’s docs say it can do this without “blowing away application state”. That means the components keep what they remember. In Vite’s terminal you see hmr update and the file name. A change to the entry file, main.tsx, makes Vite reload the whole page instead.

A strong answer adds that for React this is called Fast Refresh. The template’s linter warns that it “only works when a file only exports components”.

Sources

  • Getting Started, vite.dev: npm create vite@latest, the Node.js versions Vite requires, the -- before --template, and index.html as the entry point.
  • Features, vite.dev: hot module replacement, “does NOT perform type checking”, CSS imports, and the tsconfig.json settings Vite follows, jsx among them.
  • Static Asset Handling, vite.dev: the public folder.
  • Deploying a Static Site, vite.dev: npm run build, dist, and that vite preview is not a production server.
  • Server Options, vite.dev: port 5173, trying the next port, and strictPort.
  • Build a React app from Scratch, react.dev: Vite as a build tool for React, and the react-ts template command.
  • Editor Setup, react.dev: VS Code.
  • createRoot, react.dev: what createRoot and render do.
  • StrictMode, react.dev: the checks are development-only, what is run twice, and dimmed logs with React DevTools.
  • Importing and Exporting Components, react.dev: default and named exports.
  • TSConfig: jsx, typescriptlang.org: what "jsx": "react-jsx" does.
  • Download Node.js, nodejs.org: the LTS version.
  • about_Execution_Policies, Microsoft: PowerShell’s execution policy.
  • Keyboard shortcuts in Terminal on Mac, Apple: “Control-C”.
  • Node.js Releases, nodejs.org: LTS versions “are ready for general use”.
  • script type, MDN: type="module".
  • HTTP caching, MDN: a hash in the address lets browsers keep a file for a long time.
  • Build Options, vite.dev: the gzip size report.
  • Command line crash course, MDN: terminals on Windows, macOS and Linux.
  • Every command, file, version, message and number in this part comes from a real run on Linux with Node.js 24.18.0: create-vite 9.2.1, Vite 8.3.3, React 19.3.0, TypeScript 6.0.3 and Oxlint 1.87.0, made for this post. What the browser showed comes from headless Chromium 151 against our own dev server. The production render counts come from running the built file in jsdom.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.