知

模块一 · 起步与渲染

第 1 / 18 节18 分钟阅读型

起步:用 Vite 跑通第一个 React 应用

从装环境到跑起第一个组件,讲清 Vite 与 Next.js 的选择、入口文件做了什么、四个脚本的差异,以及 Fast Refresh 与 StrictMode 的实际影响。

读完这一节,你能做到

  1. 判断项目该用 Vite 还是 Next.js,而不是跟风选。
  2. 说清 main.tsx 里 createRoot 与 StrictMode 各自做了什么。
  3. 读懂目录结构与脚本,知道生产产物从哪来。
  4. 定位"改了没生效"与"开发环境请求两次"这两类常见现象。

一、环境与工具

工具要求检查不满足的后果
Node18 LTS 及以上node -vVite / Next 启动失败
包管理器npm 9+ / pnpm 8+npm -v依赖树漂移
编辑器VS Code + ESLint 插件扩展面板语法错误只能运行时发现
浏览器React Developer Tools开发者工具多两个面板看不到组件树与 props

React DevTools 的两个面板分工:

  • Components:组件树、props、hooks 状态,可直接改值试探。
  • Profiler:录制一次交互,看哪些组件渲染、各花多久。这是"重渲染"问题的第一现场。

二、Vite 还是 Next.js

维度Vite + ReactNext.js
定位纯客户端 SPA全栈框架,含 SSR/RSC
路由自己装 React Router文件路由
首屏 SEO需要额外方案内置
数据获取客户端请求服务端/客户端均可
学习成本低,只学 React中,要学框架约定
适用后台系统、内部工具、需要强交互的 SPA面向搜索的站点、需要服务端能力的应用

判断标准只有一条:首屏是否需要被搜索引擎抓取,或是否需要在服务端拿数据。后台管理 99% 用不上 SSR,先跑 Vite 更省心。

三、起项目与脚本

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
{
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  }
}
  • dev:不打包,按需编译,浏览器原生 ESM 加载。
  • build:先做类型检查(tsc -b),再产出 dist/。
  • preview:本地静态托管产物,用来验证生产行为。

注意:tsc -b 只检查 .ts/.tsx,不检查运行时的 props 类型。TypeScript 在 React 里是编译期约束,不是校验器。

四、入口文件在做什么

// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";

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

四件事:

  1. document.getElementById("root")! 找到 index.html 里的挂载点。! 是告诉 TS"我确定它存在"。
  2. createRoot 创建 React 18+ 的根,取代了 ReactDOM.render(React 19 已移除旧 API)。
  3. <StrictMode> 在开发环境故意双调用组件函数与 effect,用来暴露不纯的渲染与缺失的清理。
  4. .render(<App />) 触发首次渲染;此时只是算出元素树,DOM 在 commit 阶段才更新。

StrictMode 引起的"请求两次"不是 bug。它在开发环境模拟"组件挂载→卸载→再挂载",目的就是让你发现没写清理的 effect。生产环境只执行一次。处理方式是补清理、把副作用写纯,而不是删掉 StrictMode。

五、目录结构与组件

my-app/
├─ index.html          # 挂载点 <div id="root">
├─ vite.config.ts
├─ tsconfig.json
└─ src/
   ├─ main.tsx         # 入口
   ├─ App.tsx          # 根组件
   ├─ components/      # 可复用组件
   ├─ hooks/           # 自定义 Hook
   ├─ pages/           # 页面
   ├─ api/             # 请求层
   └─ assets/

第一个组件:

// src/components/Counter.tsx
import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button className="counter" onClick={() => setCount((c) => c + 1)}>
      {count}
    </button>
  );
}

三个约束:

  • 组件名必须大写,否则 JSX 会当成原生标签。
  • 返回单一根节点(Fragment <>...</> 可以包多个)。
  • 组件函数必须纯:同样的 props/state 必须得到同样的输出,不能在里面发请求或改全局。

六、JSX 编译与 Fast Refresh

JSX 不是字符串模板,编译后是一次函数调用:

const el = <button className="btn">点击</button>;
// 编译后(自动运行时)
const el = jsx("button", { className: "btn", children: "点击" });

React 17 起使用自动运行时,文件里不需要再 import React;只有用到 React.xxx 时才显式引入。

Fast Refresh 的行为要记清:

改动行为
只改组件函数体保留 state,热替换
改 hooks 顺序或数量整组件重新挂载,state 丢失
改文件导出结构可能整页刷新
改非组件文件(如工具函数)依赖它的模块全部刷新

所以"改了一行代码输入框内容清空"通常不是 bug,而是 Hook 结构变了导致重挂载。

七、常见坑

  1. 还在用 ReactDOM.render,React 19 直接报错。
  2. 忘了 import { useState } from "react",或引入整个 React 只为用 useState。
  3. 组件名小写,JSX 把它当原生标签,页面显示空标签。
  4. 在 index.html 里重复引入脚本,导致挂载两次。
  5. 删除 StrictMode 来"解决"双请求,掩盖了真正的清理缺失。
  6. 用 as any 消掉 props 类型错误,类型检查形同虚设。
  7. 生产直接打开 dist/index.html,file:// 下资源 404。

八、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 起一个 Vite + TS 项目,删掉示例,只保留 App.tsx 与 Counter.tsx。
  2. 在 StrictMode 下给组件加一个 useEffect 请求,观察请求次数;再补上清理函数,解释差异。
  3. 改一次 hooks 顺序,观察 Fast Refresh 后 state 是否还在。
  4. 跑 build + preview,确认产物可用。

② 让 AI 写同一题

把第 2 题交给 AI,补一句 React 19 + TS + Vite,不新增依赖。

③ 对比要盯的点

  • 它写了清理函数或 AbortController 吗?没写的话,StrictMode 双调用会打两次请求,卸载后还会 setState。
  • loading / data / error 它拆成几个 useState?有没有漏掉 error 分支?
  • 它给的目录结构,能不能直接当项目起点用?

④ 一句话取舍

它的版本“跑起来”了,但哪一处会让 StrictMode 打出两次请求?说明为什么这是特性不是 bug,而你仍然要写清理。

九、自测

  1. createRoot 取代了什么,为什么取代?
  2. StrictMode 在生产环境会双调用吗?
  3. dev 与 build 的本质区别是什么?
  4. Fast Refresh 什么情况下会丢 state?
  5. 什么时候该选 Next.js 而不是 Vite?

参考答案要点:ReactDOM.render,支持并发特性与多次渲染;不会;按需编译 vs 预打包;Hook 结构变化或导出变化;需要 SEO 或服务端能力时。

附:常见启动报错对照

新手期 80% 的报错集中在下面九条。看到报错先查表,比改代码快:

报错根因修法
Cannot find module 'react'依赖没装或装错目录在项目根执行 npm install
'React' must be in scopeJSX 运行时配置成了 classic改 jsx: "react-jsx" 或补 import
Objects are not valid as a React child直接渲染了对象渲染对象的字段,或 JSON.stringify
Each child in a list should have a unique key列表缺 key 或 key 重复用业务 id,不要用下标
Too many re-renders渲染期间直接调了 setState改成事件回调或 effect
Maximum update depth exceededeffect 依赖每次新建,且内部 setState依赖改稳定值,或加条件判断
Rendered more/fewer hooks than expected条件分支里调用了 HookHook 提到组件顶层
Hydration failed服务端与客户端首次渲染不一致随机值/环境判断移到 effect
Cannot update a component while rendering a different component渲染期触发了别的组件更新移到 effect 或事件

两条排查习惯:

  1. 先看第一条错误。后面的报错往往是它的连锁反应,尤其是 Objects are not valid as a React child 之后的一串堆栈。
  2. 看报错里的组件栈。React 会给出 at Component (file.tsx:12),直接点进去,比从入口逐层找快得多。

还有一个不属于报错但同样常见的问题:页面空白且控制台没有任何错误。这时先检查 index.html 里有没有 <div id="root">,再检查 main.tsx 是否真的被执行(Network 里有没有加载到入口模块)。挂载点写错时,React 不会报错,只会安静地什么都不做。

排查顺序建议固定为:依赖是否安装 → 入口是否被加载 → 挂载点是否存在 → 首个组件是否抛错。四步走完,绝大多数“页面空白”都能定位到具体一层,而不是靠猜。另外,开发服务器控制台与浏览器控制台要同时看:编译错误只出现在终端,运行时错误只出现在浏览器,只看一边很容易得出错误结论。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解