React 学习笔记 · 第 1–2 周
持有者:Kevin 阶段:Phase 1 — React 基础 涵盖:组件、props、JSX、state、事件、useState 核心心智模型:UI = f(state)
第 1 周:Components & Props(组件与 props)
1. 环境搭建(Vite)
npm create vite@latest my-react-week1 -- --template react
cd my-react-week1
npm install
npm run dev # dev server → http://localhost:5173
同一个 Vite 项目用到 Phase 1 结束就够了,不用每周新建。每周改
src/App.jsx即可。
2. 组件的定义(最重要)
组件是一个接收 props、返回 UI 的函数。 A component is a function that takes props and returns UI.
这个定义记牢,后面 state 和重新渲染都建立在它之上。
function App() {
return <h1>Hello React</h1> // 一个函数,返回 UI
}
3. JSX 不是 HTML
JSX 长得像 HTML,但会被编译成 JavaScript。两个常见坑:
// HTML 用 class,JSX 用 className
<div className="card">...</div>
// 自闭合标签必须显式闭合
<img src="photo.jpg" /> // ✅
<img src="photo.jpg"> // ❌ JSX 报错
4. Props(组件的配置参数)
类比:Figma 里一个 Button 组件有 label、variant、size 等属性。React 里这些就是 props —— 从外部传进组件的数据。
// 定义:props 作为参数传入
function ProfileCard({ name, role, photo }) { // 直接解构(destructuring)
return (
<div>
<img src={photo} alt={name} />
<h2>{name}</h2>
<p>{role}</p>
</div>
)
}
// 使用:像 HTML 属性一样传数据
<ProfileCard name="Kevin" role="Design Engineer" photo="..." />
关于 props 的三个结论(第 1 周 exit check):
- 组件是什么? 接收 props、返回 UI 的函数。
- props 流向? 由父组件单向传到子组件(one-way / 单向)。
- 为什么子组件不能改 props? props 是只读的,来自父组件,只能在父组件里改。
5. 组件名必须 PascalCase(大写开头)
不是风格,是 React 的硬规则:
<profilecard /> // ❌ React 当成原生 HTML 标签,静默失败
<ProfileCard /> // ✅ React 识别为你的组件函数
小写 = 原生 HTML 标签,大写 = 你定义的组件。 命名规范叫 PascalCase:每个单词首字母大写(
ProfileCard、UserAvatar)。
6. 花括号 {} = 进入 JS 表达式模式
{} 不是”嵌入代码块”,而是:退出 HTML 模式,把里面当 JavaScript 表达式求值。
<h2>{name}</h2> // 读变量
<h2>{1 + 1}</h2> // → <h2>2</h2>
<h2>{getName()}</h2> // 调用函数,用返回值
关键约束:必须是表达式(expression),不能是语句(statement)。
{name} // ✅ 表达式,产生一个值
{age > 18 ? "adult" : "minor"} // ✅
{if (name) {...}} // ❌ if 是语句,JSX 用不了
把 {} 想成一个插值槽(slot),往里丢一个 JS 值。
7. 什么时候需要 {}?
不是纯字符串的值,都需要花括号。
name="Kevin" // 字符串,引号就够
count={42} // 数字
isActive={true} // 布尔值
skills={["design", "program"]} // 数组(注意是方括号!)
user={{ name: "Kevin", age: 31 }} // 对象(双层花括号)
onClick={handleClick} // 函数引用
⚠️ 易错点:
skills={"design","program"} // ❌ 逗号运算符,只传了 "program"
skills={["design","program"]} // ✅ 数组
skills='["design","program"]' // ❌ 这是字符串,不是数组
8. 用 .map() 渲染列表 + key
{skills.map((skill) => (
<span key={skill}>{skill}</span> // ✅ 每项必须有 key
))}
为什么需要 key? React 用 key 追踪列表里每个元素的”身份”,列表更新时知道谁是谁,从而高效重渲染。缺 key 会报警告。 元素值唯一时,可直接用值作 key。
9. Fragment <>...</>
返回多个元素,又不想在 DOM 里多包一层 <div>:
function ProfileCard({ name, role }) {
return (
<>
<h2>{name}</h2>
<p>{role}</p>
</>
)
}
<>...</> 叫 Fragment,是个”隐形容器”。
第 2 周:State & Events(状态与事件)
核心心智模型
UI = f(state) —— UI 是 state 的函数。 改变 state,React 自动重新渲染 UI。
1. useState 基础
import { useState } from "react"
function LikeButton() {
const [count, setCount] = useState(0)
// ↑读 ↑写 ↑初始值
function handleClick() {
setCount(count + 1)
}
return <button onClick={handleClick}>♥ {count}</button>
}
count—— 当前状态值(只读 read-only)setCount—— 唯一的写入入口useState(0)——0是初始值;useState(false)初始值就是false
2. setCount 返回什么?
setCount返回undefined,它不返回值。
它的职责不是计算,而是通知 React:“状态变了,请重新渲染。”
执行流程:
点击按钮
→ handleClick() 运行
→ setCount(count + 1) 被调用
→ React 安排一次重新渲染(re-render)
→ 组件函数再次运行,这次 count 是新值
→ React 更新 DOM
setCount是触发器(trigger),不是计算器(calculator)。
3. count + 1 vs count += 1
count + 1 // 只计算出一个新值,count 本身不变
count += 1 // 直接修改 count(= count = count + 1)
在 React 里不能 count += 1 来更新 state —— 直接改变量 React 不知道,不会重渲染。
setCount(count + 1) // ✅ "嘿 React,下一个 count 应该是这个值"
count += 1 // ❌ 直接改变量,React 永远不知道
count在组件内只读;setCount是唯一的写入门。和 props 同一个原则:React 控制所有写入,你只负责告诉它新值应该是什么。
setCount(...) 括号里的东西 = 下一个 count 的值。
setCount(0) // 重置为 0
setCount(count + 10) // 跳 10
setCount(99) // 写死成 99
4. useState 的底层原理(数组 + 索引)
useState 底层用一个普通数组实现。每次调用 useState,React 把值存在数组的固定位置(索引)。
// 极简版原理
const state = []
let cursor = 0
function useState(initialValue) {
const index = cursor
if (state[index] === undefined) state[index] = initialValue
const setState = (newValue) => {
state[index] = newValue
rerender()
}
cursor++
return [state[index], setState]
}
推论:hooks 必须每次以相同顺序调用 —— 不能放在 if 或循环里:
if (cond) {
const [count, setCount] = useState(0) // ❌ 打乱索引追踪
}
const [count, setCount] = useState(0) // ✅ 永远写在顶层
某次渲染跳过一个 hook,后面所有 hook 的索引都会错位,React 读到错误的值。
5. State 是隔离且私有的
State is isolated and private —— 每个组件实例拥有自己独立的数组。
<ProfileCard /> // 自己的 state 数组
<ProfileCard /> // 自己的 state 数组
<ProfileCard /> // 自己的 state 数组
点击其中一个,不影响其他 —— 因为每个**实例(instance)**拥有各自的状态存储,互不干扰。
6. 事件处理函数要写在组件内部(闭包)
function LikeButton() {
const [count, setCount] = useState(0)
function handleClick() {
setCount(count + 1) // 能看到 count 和 setCount
}
return <button onClick={handleClick}>♥ {count}</button>
}
handleClick 需要访问 count 和 setCount,而它们只存在于组件函数内部,所以 handleClick 必须定义在组件里面。
这是 JavaScript 的闭包(closure):内层函数能访问外层函数的变量。 实用结论:需要用到 state 的函数,就定义在组件内部。
7. useState 存在内存里 —— 刷新会重置
useState → 组件挂载期间存活(刷新即清空)
localStorage → 跨刷新保留,需手动清除
database → 永久
第 2 周项目(计数器、主题切换)用 useState 正合适,不需要持久化。需要”刷新不丢”是 Phase 3+ 的事。
8. 布尔状态 + 条件渲染(主题切换)
function App() {
const [isDark, setIsDark] = useState(false)
function handleToggle() {
setIsDark(!isDark) // 布尔值翻转 true↔false
}
return (
<div className={isDark ? "dark" : "light"}>
<button onClick={handleToggle}>
{isDark ? "☀️ Light mode" : "🌙 Dark mode"}
</button>
</div>
)
}
布尔值翻转:
!false === true
!true === false
三元运算符(ternary operator) —— JSX 版的 if/else:
condition ? valueIfTrue : valueIfFalse
isDark ? "dark" : "light"
为什么 JSX 里不能直接用
if?因为{}里只能放表达式,if是语句。三元运算符是表达式,所以可用。
9. 引入 CSS
import "./App.css" // ./ = 和当前文件同一目录
src/
App.jsx ← 你在这里
App.css ← 同级,所以 './App.css'
其他路径写法:'./styles/App.css'(深一层)、'../App.css'(上一层)。
第 2 周 Exit Check(自测)
- 为什么改普通变量不触发重渲染,而
setCount会? → React 只在 state 改变时重渲染;普通变量改了 React 不知道。 useState(false)是什么意思? → 初始状态是false。- 什么是条件渲染,为什么 JSX 里不能直接用
if? → 根据状态显示不同 UI;{}里只能放表达式,if是语句,所以用三元运算符。
踩过的坑速查表
| 坑 | 错误写法 | 正确写法 |
|---|---|---|
| 组件名小写 | <profilecard /> | <ProfileCard /> |
| 数组传 props 用逗号 | skills={"a","b"} | skills={["a","b"]} |
| map 缺 key | <span>{x}</span> | <span key={x}>{x}</span> |
| onClick 拼错 | conClick={fn} | onClick={fn} |
| 直接改 state | count += 1 | setCount(count + 1) |
| 事件函数写在组件外 | (访问不到 state) | 写在组件内部 |
下一步:第 3 周 —— 列表与表单
预告概念:.map() 渲染列表(已接触)、受控输入框(controlled input,表单值存在 state 里)。
项目:todo list —— 增加、删除、标记完成。