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 组件有 labelvariantsize 等属性。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):

  1. 组件是什么? 接收 props、返回 UI 的函数。
  2. props 流向? 由父组件单向传到子组件(one-way / 单向)。
  3. 为什么子组件不能改 props? props 是只读的,来自父组件,只能在父组件里改。

5. 组件名必须 PascalCase(大写开头)

不是风格,是 React 的硬规则:

<profilecard />   // ❌ React 当成原生 HTML 标签,静默失败
<ProfileCard />   // ✅ React 识别为你的组件函数

小写 = 原生 HTML 标签,大写 = 你定义的组件。 命名规范叫 PascalCase:每个单词首字母大写(ProfileCardUserAvatar)。


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>
}

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 需要访问 countsetCount,而它们只存在于组件函数内部,所以 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(自测)

  1. 为什么改普通变量不触发重渲染,而 setCount 会? → React 只在 state 改变时重渲染;普通变量改了 React 不知道。
  2. useState(false) 是什么意思? → 初始状态是 false
  3. 什么是条件渲染,为什么 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}
直接改 statecount += 1setCount(count + 1)
事件函数写在组件外(访问不到 state)写在组件内部

下一步:第 3 周 —— 列表与表单

预告概念:.map() 渲染列表(已接触)、受控输入框(controlled input,表单值存在 state 里)。 项目:todo list —— 增加、删除、标记完成。