从零开始学前端 | 第二十九章:State、事件与受控组件
本章定位上一章我们已经从“会写组件”走到了“会让组件接收外部数据”。你已经理解了这些非常关键的内容什么是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。五、先认识useStateuseState是 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 语境。例如onClickonChangeonSubmit2. 当前阶段最实用的理解你可以先把 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}加一/buttonbutton 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 typesubmit搜索/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 typesubmit提交信息/button /form ); }1. 这个例子在训练什么它在训练你建立下面这条非常重要的链路每个输入字段都可以对应一个状态。每个字段都通过value onChange形成受控关系。提交时可以直接拿到当前状态里的最新值。2. 为什么这是后面很多项目的基础因为无论你以后写登录表单注册表单搜索表单新增待办项表单编辑学生信息表单底层思路都会不断重复这条主线。十八、state、事件与受控组件三者到底是什么关系这一节是整章最关键的整合部分。如果你把这一节想清楚了这一章就算真正吃下来了。1.state负责存数据例如countkeywordstudentName2. 事件负责触发变化例如onClickonChangeonSubmit3. 受控组件负责把表单和状态绑在一起例如value{keyword}onChange{handleChange}4. 把它们串成一句话你可以把它们关系总结成用户触发事件事件更新状态状态再驱动界面和表单显示最新结果。这句话非常重要。它几乎就是 React 入门阶段很多交互的共同骨架。十九、状态先放在哪里才更合适这一点你现在不用一次学得很深但越早有意识越好。很多初学者一学会useState就会马上出现两个极端什么数据都想放成状态一上来就把状态放到很高层的组件里更稳的起点应该是先把“真正会变化且会影响当前界面”的数据放在最直接使用它的组件里。1. 当前阶段最实用的判断方式先问自己两个问题这个数据会不会变化它变了之后界面会不会跟着变如果两个答案都接近“会”它通常就值得考虑为状态。2. 先不要过早把状态到处提升如果当前只有一个小组件在用这个状态那就先放在这个小组件里。等你后面学到组件通信时再去思考状态应该提升到哪里会更合适。当前阶段先记住一句话先把状态放在最直接使用它的地方再根据需要调整。二十、在 TypeScript 项目里状态和事件怎么补简单类型你前面已经学过 TypeScript所以这里可以顺手做一点预习。如果你在 TypeScript 项目里写 React状态和事件通常也可以补上类型。先看一个非常简单的例子import { useState } from react; function SearchBox() { const [keyword, setKeyword] useStatestring(); function handleChange(event: React.ChangeEventHTMLInputElement) { setKeyword(event.target.value); } return input value{keyword} onChange{handleChange} /; }1. 当前阶段最值得先看什么先看两处就够了useStatestring()说明这是字符串状态React.ChangeEventHTMLInputElement说明这是输入框变化事件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 交互开发的主线了。二十六、下一篇预告下一章我们会进入从零开始学前端 | 第三十章列表渲染、条件渲染与组件通信到那时你会继续解决几个非常关键的问题一组数据怎么渲染成一组界面元素某一块内容要不要显示怎么根据状态决定父组件和子组件之间怎么继续协作也就是说下一章开始我们会从“单个组件内部的状态与交互”继续走到多种界面结构和组件关系的进一步组织。