本章定位
上一章,我们已经从“会写组件”走到了“会让组件接收外部数据”。
你已经理解了这些非常关键的内容:
- 什么是
props。 - 父组件可以通过
props给子组件传值。 - 组件复用的核心,是把固定结构和变化内容分开。
这一步非常重要。
因为你已经知道:
React 组件不仅能描述界面,还能根据外部传入的数据显示不同结果。
但如果你继续往下写,很快又会遇到一个新的问题:
有些数据并不是父组件传进来的,而是组件自己会变化的。
例如:
- 点击按钮后,计数要加一。
- 在输入框里输入内容后,页面要实时显示最新文字。
- 打开和关闭弹窗时,显示状态要变化。
- 表单输入变化后,提交按钮和提示信息也可能跟着变。
这些场景有一个共同点:
数据在组件运行过程中会变化,而且这种变化会影响界面。
这时候,光靠props就不够了。
因为props更像是:
外部传进来的配置。
而我们现在要学习的,是:
组件自己如何记住、更新和使用那些会变化的数据。
这正是state要解决的问题。
所以你可以把这一章理解成:
从“接收外部数据”,走到“管理组件内部会变化的数据”。
本章学习目标
学完这一章后,你应该能做到:
- 理解什么是
state。 - 理解为什么
state和普通变量不是一回事。 - 知道
state和props最基础的区别。 - 学会使用
useState写最基础的状态。 - 理解“状态变化如何驱动界面更新”。
- 掌握 React 中最常见的事件处理方式。
- 知道点击事件和输入事件最基础的写法。
- 理解什么是受控组件。
- 学会用
state + onChange管理输入框内容。 - 理解
state、事件和受控组件之间的关系。 - 避开这一阶段最常见的几个坑。
- 为下一章学习列表渲染、条件渲染与组件通信做好准备。
一、从“接收外部数据”到“组件自己会变化”
上一章你学的是:
父组件把数据传给子组件。
这一章你要开始学的是:
组件自己的数据怎么变化。
这两个问题很像,但本质并不一样。
先看两个例子。
1. 这是props更擅长的场景
例如一张课程卡片:
- 标题是什么
- 描述是什么
- 难度是什么
这些内容往往是父组件决定后传进来的。
所以它很适合用props。
2. 这是state更擅长的场景
例如一个计数器:
- 初始是
0 - 点击一次按钮变成
1 - 再点一次变成
2
这里的数字不是父组件每次都帮你改好再传进来,而是组件自己在交互过程中不断变化。
再比如输入框:
- 一开始是空字符串
- 用户输入
R - 再输入
Re - 再输入
React
这个变化过程,本身就发生在组件内部。
所以从这一章开始,你要建立一个很关键的区分:
- 外部给我的数据,经常是
props。 - 我自己在运行中会变化的数据,经常是
state。
二、什么是state
先给一个当前阶段最够用的理解:
state是组件内部会变化,并且会影响界面显示的数据。
这个定义一定要慢慢消化。
它其实包含了三个关键词:
- 组件内部
- 会变化
- 会影响界面
1. 什么叫“组件内部”
意思是这份数据通常由当前组件自己管理。
例如:
- 当前数量是多少
- 输入框内容是什么
- 弹窗是打开还是关闭
- 当前选中了哪个分类
2. 什么叫“会变化”
它不是固定死的。
例如:
- 数量会增加或减少
- 输入内容会不断变化
- 开关状态会在
true和false之间切换
3. 什么叫“会影响界面”
这点特别关键。
因为不是所有变量都值得成为state。
真正适合放进state的,往往是:
一旦它变了,页面上某些内容也应该跟着变。
例如:
count变了,页面上的数量文本要变keyword变了,输入框显示的值要变isOpen变了,弹窗显示状态要变
所以你可以先记住一句话:
state不是随便一个变量,而是“会驱动界面变化的数据”。
三、state和props最基础的区别是什么
上一章刚学完props,这一章最容易混乱的地方就是:
props和state到底差在哪?
你现在可以先用最朴素的方式理解:
props更像外部传进来的输入。state更像组件内部自己管理的变化数据。
1. 先看一个很直观的对比
props:父组件告诉你“这张卡片该显示什么”state:组件自己记住“当前计数是多少”
2. 为什么这个区分很重要
因为后面你在看 React 代码时,经常要先判断:
这份数据到底应该从外部传进来,还是应该由组件自己维护?
当前阶段不用追求一步判断百分百准确。
你先记住一个非常稳的起点就够了:
- 来自父组件的,多半先看
props。 - 组件自己变化的,多半先看
state。
四、为什么普通变量不等于state
这一节非常关键。
很多人第一次学 React 状态时,最自然的想法是:
那我直接用
let定义一个变量,不就行了吗?
看上去很合理,但在 React 里,这往往不够。
先看一个“看起来像能工作”的例子:
function Counter() { let count = 0; function handleAdd() { count += 1; console.log(count); } return ( <section> <p>当前数量:{count}</p> <button onClick={handleAdd}>加一</button> </section> ); }1. 这段代码的问题在哪里
它的问题不是 JavaScript 语法错了,而是:
React 不会把这个普通变量当成“应该驱动界面更新的状态”来处理。
也就是说,你在函数里改了count,并不等于 React 就知道:
页面现在应该重新显示最新结果了。
2. 这一章最关键的认知转弯是什么
你要开始慢慢建立这种思维:
在 React 里,想让“数据变化带动界面变化”,不能只改普通变量,而要用 React 认可的状态机制。
这就是为什么我们接下来要认识useState。
五、先认识useState
useState是 React 提供的一个工具,用来帮助组件:
记住状态,并在状态变化时更新界面。
当前阶段你不必急着钻“Hook”这个词的细节。
你只需要先把useState理解成:
用来创建和更新组件状态的基础工具。
1. 先看最常见的写法
import { useState } from "react"; function Counter() { const [count, setCount] = useState(0); return <p>当前数量:{count}</p>; }2. 这行代码里最重要的是什么
最核心的是这一句:
const [count, setCount] = useState(0);你可以先把它拆开理解成两部分:
count:当前状态值setCount:用来更新这个状态的函数
3.0是什么意思
这里的0是初始值。
也就是:
组件第一次显示时,
count一开始是多少。
如果你写的是:
useState("")那初始值就是空字符串。
如果你写的是:
useState(false)那初始值就是false。
4. 当前阶段最稳的记法
你可以先用一句非常实用的话记住:
useState(初始值)会给你“当前状态值”和“更新状态的函数”。
这已经足够支撑你写出最基础的状态逻辑了。
六、第一个真正可用的计数器组件
现在我们把state真正用起来。
import { useState } from "react"; function Counter() { const [count, setCount] = useState(0); function handleAdd() { // 点击后更新状态,界面会显示最新数量 setCount(count + 1); } return ( <section> <p>当前数量:{count}</p> <button onClick={handleAdd}>加一</button> </section> ); }1. 这段代码做了什么
它做了三件非常重要的事:
- 用
useState(0)创建了一个初始值为0的状态。 - 用
handleAdd在点击按钮时更新状态。 - 在 JSX 里把
count显示出来。
2. 为什么它比普通变量版本更对路
因为这里不是在随手改一个普通变量,而是在调用:
setCount(...)这等于是在明确告诉 React:
这个状态变了,你应该按新的状态去更新界面。
3. 当前阶段先别急着追所有细节
你现在先稳定记住这一条主线就够了:
- 用
useState存状态 - 用
setXxx更新状态 - 在界面中读取状态值
这就是 React 入门阶段最核心的一条工作流。
七、状态变化为什么会带动界面变化
这就是 React 很核心的一层思想:
数据变,界面跟着变。
在刚才的计数器例子里:
count是数据当前数量:{count}是界面
当count从0变成1,界面里的文字自然也应该跟着变。
1. 你现在最值得建立的理解
不是底层渲染细节,而是:
界面不是手动一点点改出来的,而是根据当前状态“重新描述出来的”。
这和前面学 React 第一章时讲的思路,其实是接上的。
2. 为什么state在 React 里这么重要
因为很多交互,本质上都是在回答同一个问题:
当前状态是什么?
例如:
- 当前计数是多少
- 当前输入内容是什么
- 当前是否展开
- 当前选中了哪个项目
一旦这些状态有了,界面通常就可以跟着它们变化。
所以你可以先把state看成:
连接“用户操作”和“界面结果”的中间桥梁。
八、什么是 React 事件
既然状态经常会变化,那变化通常是谁触发的?
很多时候,就是:
用户的操作。
例如:
- 点击按钮
- 输入文字
- 提交表单
- 鼠标移入
- 键盘按下
这些用户操作,在 React 里通常就是通过事件来处理。
1. React 事件和原生 DOM 事件像不像
很像。
你前面学原生 JavaScript 事件时已经见过:
clickinputsubmit
在 React 里,你依然会处理这些事情,只是写法更贴近 JSX 语境。
例如:
onClickonChangeonSubmit
2. 当前阶段最实用的理解
你可以先把 React 事件理解成:
当用户在界面上做了某个动作时,我们给这个动作绑定一个函数,让 React 去执行它。
九、最常见的点击事件写法
点击事件几乎是 React 初学阶段最常见的入口。
先看一个最简单的例子:
function LikeButton() { function handleClick() { console.log("你点击了点赞按钮"); } return <button onClick={handleClick}>点赞</button>; }1. 这里最重要的结构是什么
先抓住两部分:
handleClick:点击后要执行的函数onClick={handleClick}:把这个函数绑定给按钮点击事件
2. 为什么很多人喜欢把函数名写成handleXxx
这是一种很常见、也很清楚的命名习惯。
例如:
handleClickhandleSubmithandleInputChange
它能让你一眼看出来:
这个函数是在处理某个事件。
十、事件处理函数最常做的三件事
当事件发生后,事件处理函数里通常会做这几类事情。
1. 第一类:更新状态
这在 React 里非常常见。
例如点击按钮让数量加一:
function handleAdd() { setCount(count + 1); }2. 第二类:读取当前输入或当前数据
例如输入框变化时读取用户输入的值:
function handleChange(event) { console.log(event.target.value); }3. 第三类:阻止默认行为或执行提交逻辑
例如表单提交时:
function handleSubmit(event) { event.preventDefault(); console.log("表单提交了"); }当前阶段你可以先把事件处理函数理解成:
用户操作发生后,React 帮你执行的一段业务逻辑。
十一、为什么onClick={handleAdd}和onClick={handleAdd()}不一样
这一点是 React 初学者非常容易踩的坑。
看下面两种写法:
<button onClick={handleAdd}>加一</button><button onClick={handleAdd()}>加一</button>它们看起来只差一点点,但含义完全不同。
1.onClick={handleAdd}是什么意思
它表示:
把
handleAdd这个函数交给 React,等用户真正点击时再执行。
2.onClick={handleAdd()}是什么意思
它表示:
组件渲染时,立刻先把这个函数执行一遍。
这通常不是你想要的效果。
3. 当前阶段最稳的习惯
如果你只是想在点击时执行某个函数,优先写成:
onClick={handleAdd}先把这个基础习惯养稳,后面再慢慢理解更灵活的写法。
十二、输入框事件与onChange
除了点击事件,输入框事件也是这一章的重点。
因为一旦进入表单场景,你几乎一定会遇到:
用户输入了什么?
React 里最常见的入门写法是:
function SearchBox() { function handleChange(event) { console.log(event.target.value); } return <input onChange={handleChange} placeholder="请输入关键词" />; }1. 这里的event.target.value是什么
它可以先理解成:
当前输入框里的最新内容。
例如用户输入:
RReReact
每次变化时,你都可以通过event.target.value拿到最新值。
2. 为什么这对 React 很重要
因为很多时候你不只是想“知道用户输入了什么”,你还想:
- 把它显示到页面上
- 做校验
- 控制按钮是否可点
- 根据它筛选列表
这时候就需要把输入值接到state上。
也就是我们接下来要讲的受控组件。
十三、什么是受控组件
“受控组件”这个词第一次看起来可能有点抽象。
你现在可以先把它理解成:
表单元素的值由 React 的
state来控制。
这句话非常重要。
例如一个输入框,如果它是受控的,通常意味着:
- 输入框显示什么值,由
state决定。 - 用户输入后,通过事件去更新
state。 - 更新后的
state再反过来影响输入框显示。
1. 当前阶段最实用的判断标准
如果你看到一个输入框同时具备这两样东西:
value={某个状态}onChange={某个更新函数}
那它通常就是一个受控组件。
2. 为什么这一章一定要学它
因为在真实项目里,表单场景非常常见。
而受控组件会直接影响这些能力:
- 输入联动
- 表单校验
- 提交数据
- 实时预览
- 禁用按钮控制
所以这不是一个边缘知识点,而是 React 表单的核心基础。
十四、最基础的受控输入框
先看一个最简单、也最经典的例子:
import { useState } from "react"; function NicknameEditor() { const [nickName, setNickName] = useState(""); function handleChange(event) { // 输入框的值始终交给 state 管理 setNickName(event.target.value); } return ( <section> <input value={nickName} onChange={handleChange} placeholder="请输入昵称" /> <p>当前输入:{nickName || "还没有输入内容"}</p> </section> ); }1. 这里发生了什么
这段代码的工作流非常值得你反复看:
nickName状态一开始是空字符串。- 输入框的
value绑定了这个状态。 - 用户输入时,触发
onChange。 handleChange里调用setNickName更新状态。- 状态变了,界面里输入框和下面的文本也都跟着变。
2. 这正是“数据变,界面跟着变”
这个例子几乎就是 React 核心思想的一个小缩影。
你没有手动去改:
- 输入框的 DOM 文本
- 下面段落的文本节点
而是只做了一件事:
更新状态。
然后界面就会按最新状态重新显示。
十五、为什么它叫“受控”
这个名字其实很直白。
它叫受控,是因为:
输入框显示什么,不是它自己随便决定,而是由 React 状态来控制。
1. 什么叫“控制”
在刚才那个例子里,输入框的显示值来自:
value={nickName}这表示:
输入框现在显示什么,取决于
nickName当前是多少。
2. 为什么这很有价值
因为一旦输入值被 React 状态接住了,你就更容易做这些事:
- 实时展示当前输入内容
- 限制输入规则
- 提交前做校验
- 一键清空输入框
- 根据输入值做搜索或筛选
也就是说,受控组件不是为了“写法更复杂”,而是为了:
让表单行为更可控。
十六、表单提交时为什么常见preventDefault
一说到表单,很多人会看到这行代码:
event.preventDefault();当前阶段你可以先把它理解成:
阻止表单默认提交行为,把提交流程交给 React 自己处理。
先看一个简单例子:
import { useState } from "react"; function SearchForm() { const [keyword, setKeyword] = useState(""); function handleSubmit(event) { // 阻止表单默认刷新,改为自己处理提交逻辑 event.preventDefault(); console.log("提交关键词:", keyword); } return ( <form onSubmit={handleSubmit}> <input value={keyword} onChange={function (event) { setKeyword(event.target.value); }} placeholder="请输入搜索关键词" /> <button type="submit">搜索</button> </form> ); }1. 这里最值得你看懂什么
先看这三件事:
- 输入框值由
keyword控制。 - 输入变化时更新
keyword。 - 提交表单时,不走浏览器默认流程,而是执行自己的逻辑。
2. 为什么这在 React 里很常见
因为很多前端表单并不是:
交给浏览器直接提交到新页面。
而是:
由前端先校验、整理数据,再自己决定后续流程。
例如:
- 调接口
- 弹出提示
- 清空表单
- 更新列表
所以preventDefault()会很常见。
十七、一个包含两个字段的受控表单小例子
现在我们把思路往前推进一点。
import { useState } from "react"; function StudentForm() { const [studentName, setStudentName] = useState(""); const [cityName, setCityName] = useState(""); function handleSubmit(event) { // 阻止默认提交后,再处理自己的业务逻辑 event.preventDefault(); console.log({ studentName, cityName }); } return ( <form onSubmit={handleSubmit}> <input value={studentName} onChange={function (event) { setStudentName(event.target.value); }} placeholder="请输入姓名" /> <input value={cityName} onChange={function (event) { setCityName(event.target.value); }} placeholder="请输入城市" /> <button type="submit">提交信息</button> </form> ); }1. 这个例子在训练什么
它在训练你建立下面这条非常重要的链路:
- 每个输入字段都可以对应一个状态。
- 每个字段都通过
value + onChange形成受控关系。 - 提交时,可以直接拿到当前状态里的最新值。
2. 为什么这是后面很多项目的基础
因为无论你以后写:
- 登录表单
- 注册表单
- 搜索表单
- 新增待办项表单
- 编辑学生信息表单
底层思路都会不断重复这条主线。
十八、state、事件与受控组件三者到底是什么关系
这一节是整章最关键的整合部分。
如果你把这一节想清楚了,这一章就算真正吃下来了。
1.state负责存数据
例如:
countkeywordstudentName
2. 事件负责触发变化
例如:
onClickonChangeonSubmit
3. 受控组件负责把表单和状态绑在一起
例如:
value={keyword}onChange={handleChange}
4. 把它们串成一句话
你可以把它们关系总结成:
用户触发事件,事件更新状态,状态再驱动界面和表单显示最新结果。
这句话非常重要。
它几乎就是 React 入门阶段很多交互的共同骨架。
十九、状态先放在哪里,才更合适
这一点你现在不用一次学得很深,但越早有意识越好。
很多初学者一学会useState,就会马上出现两个极端:
- 什么数据都想放成状态
- 一上来就把状态放到很高层的组件里
更稳的起点应该是:
先把“真正会变化且会影响当前界面”的数据,放在最直接使用它的组件里。
1. 当前阶段最实用的判断方式
先问自己两个问题:
- 这个数据会不会变化?
- 它变了之后,界面会不会跟着变?
如果两个答案都接近“会”,它通常就值得考虑为状态。
2. 先不要过早把状态到处提升
如果当前只有一个小组件在用这个状态,那就先放在这个小组件里。
等你后面学到组件通信时,再去思考状态应该提升到哪里会更合适。
当前阶段先记住一句话:
先把状态放在最直接使用它的地方,再根据需要调整。
二十、在 TypeScript 项目里,状态和事件怎么补简单类型
你前面已经学过 TypeScript,所以这里可以顺手做一点预习。
如果你在 TypeScript 项目里写 React,状态和事件通常也可以补上类型。
先看一个非常简单的例子:
import { useState } from "react"; function SearchBox() { const [keyword, setKeyword] = useState<string>(""); function handleChange(event: React.ChangeEvent<HTMLInputElement>) { setKeyword(event.target.value); } return <input value={keyword} onChange={handleChange} />; }1. 当前阶段最值得先看什么
先看两处就够了:
useState<string>(""):说明这是字符串状态React.ChangeEvent<HTMLInputElement>:说明这是输入框变化事件
2. 为什么这对你后面会很有帮助
因为后面写 React + TypeScript 项目时,你会越来越常见到这些问题:
- 输入框的事件对象是什么类型
- 状态应该存字符串还是布尔值
- 组件到底接收什么数据
现在先有这个印象,后面衔接会更顺。
二十一、初学者最容易踩的几个坑
这一节建议你认真看。
因为state和事件本身不算特别难,但初学时很容易因为一些小误区卡住。
1. 坑一:用普通变量代替状态
这是最常见的问题。
你以为:
let count = 0;再加一个点击事件就够了。
但 React 真正能可靠驱动界面更新的,是状态,而不是随手定义的普通变量。
2. 坑二:有了状态,却不通过setXxx更新
例如你明明已经写了:
const [count, setCount] = useState(0);却还在想着“我能不能直接把count改掉”。
当前阶段你一定要先养成习惯:
更新状态,优先通过
setXxx。
3. 坑三:把onClick={handleAdd}写成onClick={handleAdd()}
这个前面已经讲过,但真的太常见了。
如果你只是想在点击时再执行函数,就先稳稳写成:
onClick={handleAdd}4. 坑四:输入框用了onChange,却没把value绑定给状态
如果你只写:
onChange={handleChange}却没有把输入框的值交给状态管理,那你通常还没有真正进入“受控组件”的思路。
受控组件的关键是:
value由状态提供onChange更新状态
这两步要配套理解。
5. 坑五:一学状态,就把所有数据都塞成state
不是所有变量都值得做成状态。
你要慢慢养成这个判断:
只有那些会变化、并且变化后会影响界面的数据,才优先考虑做成状态。
二十二、本章实践练习
这一章的练习重点,是把“状态驱动界面”这条主线真正练熟。
1. 练习 1:实现一个最基础的计数器组件
请你完成一个计数器,至少包含:
- 当前数量显示
- 一个“加一”按钮
如果你状态比较稳,可以再补一个:
- “减一”按钮
这个练习的重点是:
让你熟悉
useState、事件处理函数和状态更新的配合方式。
2. 练习 2:实现一个输入框实时预览组件
请你写一个输入框组件,要求做到:
- 用户输入什么,页面下方就实时显示什么。
- 如果输入为空,显示一段占位提示。
这个练习会帮你真正把:
value + onChange + state
这一套受控组件基础写法练熟。
3. 练习 3:实现一个简单的搜索表单
请你写一个包含:
- 输入框
- 提交按钮
的小表单。
要求:
- 输入框是受控组件。
- 提交时阻止默认行为。
- 提交后输出当前关键词。
这个练习的重点是:
理解表单事件和状态之间的配合。
4. 练习 4:把前面的待办事项应用改写成 React 版本雏形
你不需要一次做完整功能。
先从最基础的部分开始:
- 用状态保存输入框内容。
- 用按钮点击事件新增一条待办文本。
- 把新增结果先简单显示到页面上。
这个练习会让你开始把:
- 状态
- 点击事件
- 输入事件
- 表单思路
真正串到一个小场景里。
二十三、学习重点提示
这一章请你重点记住下面这些话:
state是组件内部会变化、并且会影响界面显示的数据。props更像外部输入,state更像组件内部自己管理的数据。- 普通变量不等于 React 状态,想让界面跟着变,要用状态机制。
useState会给你“当前状态值”和“更新状态的函数”。- 更新状态时,优先通过
setXxx来完成。 - React 事件的核心作用,是在用户操作发生后执行相应逻辑。
- 受控组件的关键特征是:
value由状态提供,onChange负责更新状态。 state、事件和受控组件是连在一起的:事件更新状态,状态驱动界面。- 不是所有数据都要做成状态,优先考虑那些会变化且会影响界面的数据。
- 先把状态放在最直接使用它的组件里,再根据需要调整。
如果你只记一句话,请记住:
React 交互的核心主线通常是:用户触发事件,事件更新状态,状态再驱动界面显示最新结果。
二十四、本章小结
这一章,我们正式把 React 从“组件接收外部数据”推进到了“组件自己管理变化数据”。
你已经理解了:
- 什么是
state - 为什么普通变量不等于 React 状态
useState最基础的用法- 为什么状态变化会带动界面更新
- React 中点击事件、输入事件和表单事件的基础写法
- 什么是受控组件
- 为什么受控组件会成为 React 表单的核心基础
state、事件和受控组件三者之间的关系
更重要的是,你开始真正建立起 React 交互的核心思路:
我们不再到处手动修改页面,而是通过事件去更新状态,再让界面跟着状态变化。
这一步非常关键。
因为从这里开始,你就已经不只是会写“静态组件”了,而是在开始进入:
真正有交互能力的 React 组件阶段。
二十五、课后思考题
请你认真思考下面这些问题:
- 为什么说
state和普通变量不是一回事? props和state最基础的区别是什么?- 为什么说 React 的关键思想之一是“数据变,界面跟着变”?
- 为什么
onClick={handleAdd}和onClick={handleAdd()}含义不同? - 受控组件为什么一定离不开
value和onChange的配合? - 为什么表单提交时常常会用到
preventDefault()? - 什么样的数据更适合放进
state?
建议你把这些问题用自己的话写下来。
只要你能把这些问题讲清楚,说明你已经真正开始进入 React 交互开发的主线了。
二十六、下一篇预告
下一章我们会进入:
从零开始学前端 | 第三十章:列表渲染、条件渲染与组件通信
到那时,你会继续解决几个非常关键的问题:
- 一组数据怎么渲染成一组界面元素
- 某一块内容要不要显示,怎么根据状态决定
- 父组件和子组件之间怎么继续协作
也就是说,下一章开始,我们会从“单个组件内部的状态与交互”继续走到:
多种界面结构和组件关系的进一步组织。