news 2026/10/13 18:12:57

React基础教程:搞懂JSX、组件、props与state核心概念

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React基础教程:搞懂JSX、组件、props与state核心概念

写React基础教程的人很多,但大多要么太薄,要么直接默认你会ES6、懂工程化。这篇文章我打算换个思路,从“JSX到底是什么”这个最容易被误解的点切入,把语法、组件、props、state这几个核心概念揉碎了讲清楚,顺带把我这几年带新人时常被问到的、还有自己当年踩过的坑都摆出来。目标是让完全没接触过React的人也能顺畅读下来,看完能自己写出一个像样的组件。

1. 先搞清楚JSX是什么,以及它为什么不是模板

1.1 从一次面试说起:JSX其实只是函数调用的语法糖

先记住一个判断:JSX不是HTML,更不是模板字符串。React之所以选择JSX,不是为了在JavaScript里写HTML,而是为了让开发者能用声明式的方式描述UI。它的底层逻辑是把XML-like的语法通过编译器(比如Babel)转换成React.createElement的调用。

// 你写的: const element = <h1 className="title">你好,React</h1>; // 编译后大致是: const element = React.createElement( 'h1', { className: 'title' }, '你好,React' );

这串转换搞明白后,JSX里那些“反直觉”的规则就都能理解了:为什么属性要用className而不是class?因为class是JavaScript的保留字;为什么style要传对象?因为createElement的第二个参数本来就是一个对象;为什么组件标签可以像HTML标签一样嵌套?因为它最终只是一层层函数调用的嵌套。

很多新手把JSX类比Vue的模板,这是个大误区。模板是字符串解析,JSX是表达式参与JavaScript逻辑的流转,它可以放在if里、赋值给变量、作为函数参数传递,这正是JSX灵活性的来源。

1.2 JSX里你一定会踩的表达式嵌入坑

JSX支持用单花括号{}插入任意JavaScript表达式。重点在于“表达式”三个字:if、for、switch这些是语句,不能放进{}里。

{/* 错误示范 */} { if (isLoggedIn) { <p>欢迎回来</p> }} {/* 正确写法:用三元表达式或提前return */} { isLoggedIn ? <p>欢迎回来</p> : <button>登录</button> }

如果你在组件里写{ if (...) },编译阶段就直接报错,这不是“能不能跑”的问题,是Babel解析JSX时根本过不去。但凡是需要条件渲染,要么三元运算符、要么短路逻辑&&、要么在函数体内用if提前return,这是React的思维习惯。

另外注意,{}里的表达式会做隐式类型转换。布尔值false、null、undefined不会渲染任何东西,但数字0会渲染成“0”,这在渲染数组时特别容易出事。

{/* 这个看起来是对的,但list为空时会渲染出一个"0" */} { list.length && list.map(item => <p key={item.id}>{item.name}</p>) } {/* 应该这样写 */} { list.length > 0 && list.map(item => <p key={item.id}>{item.name}</p>) }

这个问题我在代码评审里见过太多次了。list.length && ...在list为空数组时,0会被当作有效内容渲染到页面上,排查起来还不好找,因为不是所有数据源都会稳定返回空数组。

1.3 JSX的属性规则:驼峰命名、表达式和布尔值

JSX标签的三大属性规则,按出现频率排序:

  • 属性名统一用驼峰式:tabIndex、onClick、maxLength,不是为了好看,是React的事件系统和原生DOM属性映射是依赖驼峰命名的。
  • 属性值用引号传字符串,用花括号传表达式:<input placeholder="请输入">是字符串,<input maxLength={10}>是数字。
  • 布尔属性要谨慎:传true可以直接省略值(<input disabled />等于disabled={true}),但传false必须显式写出来,否则React也会当true处理,你写的时候要理解这个底层逻辑。

还有style属性,它接收一个对象,键名是驼峰式CSS属性名:

const containerStyle = { display: 'flex', justifyContent: 'space-between', backgroundColor: '#f5f5f5', padding: '12px 16px' }; <div style={containerStyle}>内容</div>

在组件里写内联style不推荐用于复杂样式,但动态样式(比如根据状态改变颜色)用它是合理的。CSS-in-JS的各类方案归根结底也是围绕JS对象做文章,理解了这一点,后续用任何样式库都不会心里没底。

2. 组件:React世界的积木

2.1 函数组件与类组件,新手该从哪里切入

组件就是“把一个区域的UI做好,并且允许外部传参”的函数。2024年这个时间点,新项目基本上没有理由再写类组件了。函数组件配Hooks是React官方推荐的写法,而且面试时如果只会给类组件,会显得知识体系停在2019年。

函数组件长这样:

function WelcomeMessage({ username, messageCount }) { return ( <div className="welcome-box"> <h2>欢迎回来,{username}</h2> <p>你有 {messageCount} 条未读消息</p> </div> ); }

类组件长这样(了解即可,主要是读老代码用):

import { Component } from 'react'; class WelcomeMessage extends Component { render() { const { username, messageCount } = this.props; return ( <div className="welcome-box"> <h2>欢迎回来,{username}</h2> <p>你有 {messageCount} 条未读消息</p> </div> ); } }

类组件里有this指向的问题、方法绑定的问题、生命周期函数被Hooks替代的问题,每一项都额外增加认知负担。函数组件无实例、无this,心智模型简单纯粹:输入props,输出UI。

我这几年带新人的经验是:先只学函数组件,然后用三个脚手架项目堆熟练度,再回头花半小时看类组件语法,就够用了。没必要在第一天就两头都抓。

2.2 props的流动规则,以及为什么它是只读的

props是父组件给子组件的数据。子组件可以随便用,但不能改——改了会直接报错(在严格模式下),因为React需要保证数据流是单向的、可预测的。

数据从父组件向下流动,修改数据的动作只能发生在源头。这不是React故意设限制,而是为了让你在排查问题时,永远只需要顺着一条线往上找:

function Parent() { const [count, setCount] = useState(0); return ( <section> <ChildWrapper count={count} onClear={() => setCount(0)} /> </section> ); } function ChildWrapper({ count, onClear }) { return ( <div> <p>当前值:{count}</p> <button onClick={onClear}>清空</button> </div> ); }

props层面的核心经验是:把值的更新逻辑放在真正需要这份数据的组件里。数据放在顶层,通过props一级级传下去,是“状态提升”的思路;但过度提升会把中间层变成转发站,代码可读性下降。这个度靠经验拿捏,一般建议是:只有两个或以上兄弟组件需要共享数据时,才考虑提升到共同父级。

2.3 children和组合:组件复用的另一种姿势

除了普通的属性传值,React组件还有一个特殊的props字段叫children,它代表标签体内部的内容。这让组件可以通过组合来复用,而不是通过继承。

function Card({ title, children }) { return ( <div className="card"> <div className="card-header">{title}</div> <div className="card-body">{children}</div> </div> ); } // 使用方传入任意结构 <Card title="今日推荐"> <ul> <li>React基础</li> <li>TypeScript入门</li> </ul> </Card>

children可以是任何React节点:字符串、JSX元素、数组、函数。它让组件的“外壳”和“内容”分离,是实现布局组件、通用容器组件(如弹窗、卡片、折叠面板)的基石。写得熟练后,你会发现组件的复用层次主要靠props和children两种机制就够覆盖了,绝大多数情况用不到高阶组件、render props这些进阶手段。

3. 组件加上状态,才算活过来

3.1 用useState理解state的“不可变更新”

state是组件自己维护的内部数据。React的核心循环是:数据变了,视图跟着变。但这个“变”必须通过特定的方式触发,而不是直接修改对象。

function Counter() { const [count, setCount] = useState(0); const handleIncrement = () => { setCount(count + 1); // 正确:生成新值 // count = count + 1; // 错误:直接修改,不会触发重新渲染 }; return ( <div> <p>点击次数:{count}</p> <button onClick={handleIncrement}>加1</button> </div> ); }

新手最容易绕不过来的是:为什么count不能直接改?因为在React的视图更新机制里,它需要在内存里对比新旧两份状态树,来决定哪些DOM需要更新。直接改同一个对象,新值和旧值是同一个引用,React无法判断变了没有。

处理对象和数组时也一样——要生成一个新对象/新数组,而不是改原值:

// 对象:用展开语法生成新对象 setUser(prev => ({ ...prev, name: '新名字' })); // 数组:用filter/map/concat生成新数组 setTodoList(prev => prev.filter(item => item.id !== targetId));

注意我在上面用了函数式更新prev => ...。state的更新是异步的,同一个事件循环里多次调用setCount(prev => prev + 1)会排队执行,基于最新的prev计算结果,不会互相覆盖。如果你在回调里依赖旧值做复杂计算,推荐一律用函数式写法,避免闭包捕获过期值。

3.2 React“重新渲染”到底发生在哪一步

能区分“渲染”和“渲染后”是React进阶的起点。每次setState触发后,React会重新执行组件函数,生成新的虚拟DOM树,对比后只更新真实DOM里变化的部分。

这带来两个实际影响:

  • 组件函数体里的代码每次渲染都会重新执行,所以不要在函数体里写“打印日志之外Side Effect”的代码,比如定时器、请求、DOM操作。
  • 对比过程依赖props和state的引用,所以每次渲染生成的新数组、新函数引用都不相等,这会影响子组件的重渲染。
function GrandChild() { console.log('GrandChild渲染了'); return <p>子组件</p>; } function Parent() { const [count, setCount] = useState(0); // 每次渲染都生成新的内联函数——子组件会跟着重渲染 const handleClick = () => setCount(count => count + 1); return ( <div> <button onClick={handleClick}>点击</button> <GrandChild /> </div> ); }

这个例子点一次按钮,控制台会打出两次GrandChild渲染了。问题在于父组件重渲染时,子组件没有收到任何新props,但它还是跟着重渲染了一遍。优化手段是React.memo,但这属于性能优化范畴,新手阶段只需要理解这个现象,不要一上来就到处包memo,反而容易掩盖组件职责不清晰的问题。

3.3 用useEffect处理组件生命周期和副作用

副作用指任何和UI渲染没有直接关系但组件需要的操作:请求数据、订阅事件、操作DOM、设置定时器。useEffect是处理这些的入口,它有两个参数:一个函数和一个依赖数组。

function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { let canceled = false; fetch(`/api/users/${userId}`) .then(res => res.json()) .then(data => { if (!canceled) { setUser(data); } }); return () => { canceled = true; }; }, [userId]); // 依赖:userId变化时才重新执行 return <div>{user ? user.name : '加载中...'}</div>; }

依赖数组的取值规则是:effect内部用到所有外部变量,都应该列入依赖。空数组[]表示只在挂载后执行一次。清理函数(return的部分)在依赖变化前、以及组件卸载时执行,用来取消订阅、清理定时器。

搞懂useEffect的依赖机制后,很多“请求重复发”“定时器越积越多”的问题都能对号入座。我建议新手用调试法理解它:在effect内部和清理函数里各打一条console.log,然后操作组件,观察打印顺序。

4. 从0到1搭一个React应用:环境、目录和调试

4.1 如果从零开始,前30分钟该做什么

新建一个React项目,我推荐直接使用create-react-app或vite。个人更偏向Vite,启动快、配置少,适合学习和小型项目。建项目的过程就不过多展开了,静态脚手架目的只有一个:让你拿到一个能跑的React环境。

拿到后,你先做三件事:

  1. 删掉自带的logo和示例代码。
  2. 把App组件拆出来,单独建components目录,放一个自写的卡片组件。
  3. 打开浏览器DevTools,装好React开发者工具扩展。

调试工具里最常用的几个面板:Components面板(看组件树和props/state)、Profiler(性能分析)。新手阶段主要是用Components面板确认props到底传了什么、state到底是什么值,这比打日志快得多。

4.2 事件处理与状态联动:一个完整的交互示例

把之前的知识串起来,写一个最常用的小场景:受控表单。

function SearchForm({ onSearch }) { const [keyword, setKeyword] = useState(''); const handleSubmit = (e) => { e.preventDefault(); onSearch(keyword.trim()); }; return ( <form onSubmit={handleSubmit}> <input type="text" value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder="输入关键词" /> <button type="submit">搜索</button> </form> ); }

表单里input的显示值由React的state控制,用户输入触发onChange更新state,state更新后input重新渲染——这就是“受控组件”,它的优点是真正确保了“显示在页面上的”和“存于React状态里的”永远一致,不会出现DOM游离状态。

几乎所有交互都是这个模式的变体:状态定义在组件里,事件处理器更新状态,状态驱动渲染。能够清楚地按这个闭环思考,React基础就算真正过关了。

4.3 案例:实现一个可复用的标签列表组件

这里给出一个稍微综合一点的例子,把props、state、children、事件结合起来:

function TagList({ tags, onRemove }) { if (!tags.length) { return <p className="empty">暂无标签</p>; } return ( <ul className="tag-list"> {tags.map((tag) => ( <li key={tag.id} className="tag-item"> <span>{tag.name}</span> <button type="button" aria-label={`删除标签${tag.name}`} onClick={() => onRemove(tag.id)} > 删除 </button> </li> ))} </ul> ); }

这个组件的设计思路:

  • 数据来源是父组件传入的tags数组。
  • 删除事件通过onRemove回调向上传递,数据操作留在父组件。
  • 空数组时渲染提示文案,而不是依赖外部判断。

父组件持有数据源,自己维护增删改逻辑:

function TagManager() { const [tags, setTags] = useState([ { id: 1, name: 'React' }, { id: 2, name: 'JavaScript' }, ]); const handleRemove = (id) => { setTags(prev => prev.filter(tag => tag.id !== id)); }; const handleAdd = (name) => { const newTag = { id: Date.now(), name }; setTags(prev => [...prev, newTag]); }; return ( <section> <TagList tags={tags} onRemove={handleRemove} /> </section> ); }

这就是React基础阶段的完整闭环:组件、props、state、事件处理、列表渲染。看到这里,你已经可以用React写很多实际的小工具了。

5. 常见问题与排查技巧实录

5.1 为什么我的组件没有重新渲染

排查这类问题的固定三连:

  1. 检查state是否真的变了。在setState前后打印值,确认引用是否变化。
  2. 检查是不是在错误的地方做了setState。比如在组件函数体里直接调用setState,这会死循环;在条件渲染的子组件里写了setState,父组件重渲染时会反复触发。
  3. 检查是不是传给子的props引用没变。子组件用React.memo包裹后会做浅比较,不会换引用就不重渲染。

还有一种隐蔽情况:确实能渲染,但你以为的“没渲染”其实是数据来源错了。用React DevTools看组件props和state,一眼就能定位是数据本身有问题还是渲染逻辑有问题。

5.2 key到底要怎么选,为什么不能用index

key是React追踪列表项身份的依据,用index做key在静态列表里没问题,但一旦涉及排序、增删,就会产生状态错位。

举个例子:列表第1项是输入框,key为0。用户输入内容后,列表头部新增一项,原来key为0的项变为第2项,但React认为“第0项没有变”,输入框内容不会跟着移动,视觉上就错位了。

解决思路:每条数据生成一个唯一id,最简单的是crypto.randomUUID()或Date.now()拼接,推荐优先用后端返回的id字段。

5.3 闭包包了旧值,怎么解

这是useEffect依赖问题最常见的表现。比如用户在effect里读取了一个state,但这个state没有放进依赖数组,那effect触发时读到的是上一次渲染的旧值。

解法是:直接用useEffect依赖数组列出所有用到的外部值,不用怕重复执行。依赖变化导致effect重复请求如果是个问题,说明数据流设计上可能需要调整,而不是靠闭包绕过。

5.4 事件函数里this报错(类组件场景)

如果你还在维护类组件代码,事件处理函数必须绑定this:

class MyComponent extends Component { handleClick = () => { this.setState({ count: this.state.count + 1 }); }; render() { return <button onClick={this.handleClick}>点击</button>; } }

用箭头函数声明类字段(如上写法)可以避免绑定问题,这是最简单可靠的方案。没有使用箭头函数时,原生事件回调里的this会指向调用者,不绑定就会报“Cannot read properties of undefined”。这里提一句,是因为很多老项目中还能看到这种代码,读得懂才能改得动。

6. 下一步该往哪个方向走

React基础学完,再往下花时间的地方,按优先级列一下,供还没有规划的同学参考:

  • 状态管理库(Redux / Zustand):中小项目先不急着上,多写几个页面让props层层传递的体验足够痛了再学,接受度会更高。
  • React Router:页面跳转必备,学起来很快,跟着官方教程案例写两遍就能上手。
  • 代码规范与错误边界:组件报错时的降级处理、代码分割、懒加载这类工程化话题。
  • TypeScript:React的类型推导强,项目规模一大,配合TS写会明显减少低级错误,建议尽早引入。

我不主张一口气把所有生态都学完。先把基础语法练熟,再把一个自己感兴趣的小项目(记账本、待办清单、个人信息页)从零到一写完,踩完一遍实际工程的坑,再来补齐工程化知识,这样的成长曲线最省力。

从我的实际教学体验看,绝大多数React基础阶段学不完项目的人,不是因为不够聪明,而是被“先学完所有概念再动手”的完美主义拖住了。概念掌握到本节程度就已经足够撑起第一个项目。请务必尽早写起来,React这门技术,只有亲手跑出第一个交互页面,才算真正入门。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 18:12:20

服装管理系统实战:用状态/策略/观察者模式解决库存与定价难题

简介&#xff1a;本资源是面向软件工程专业本科生及设计模式初学者的课程大作业实践项目&#xff0c;聚焦服装管理系统的架构设计与模式落地&#xff0c;帮助学习者深入理解设计模式在真实业务场景中的选型依据与实现细节。压缩包共39个文件&#xff0c;含17个Java源码&#xf…

作者头像 李华
网站建设 2026/10/13 18:11:59

自适应滑模控制原理与Matlab仿真:参数不确定系统的鲁棒设计

参数不确定这事&#xff0c;做控制的都懂&#xff0c;模型拿到手&#xff0c;标称参数看着挺准&#xff0c;一上实际系统全是偏差&#xff0c;摩擦、老化、负载变化&#xff0c;每一项都在考验控制器的鲁棒性。传统PID还能凑合&#xff0c;但要是系统本身非线性强、参数摄动范围…

作者头像 李华
网站建设 2026/10/13 18:11:18

从环境准备到版本管理:开发环境配置的底层逻辑与实战指南

聊到“环境准备”&#xff0c;很多人的第一反应是“不就是装个软件嘛&#xff0c;照着官网点下一步就行”。但真正在项目里吃过亏的人都知道&#xff0c;一个靠谱的开发环境&#xff0c;往往决定了你整个项目的体验底色。我见过太多人卡在教程的“前置准备”环节——不是下载太…

作者头像 李华
网站建设 2026/10/13 18:08:51

Solr部署模式选择:独立服务器与SolrCloud集群的实战对比

1. 从单机到集群&#xff1a;为什么Solr的部署模式选择比调优更重要很多刚接触Solr的朋友&#xff0c;拿到安装包的第一反应就是解压、启动、建索引&#xff0c;跑通了就觉得万事大吉。我见过太多项目&#xff0c;前期用单机模式跑得飞快&#xff0c;数据量一上来就开始频繁GC、…

作者头像 李华
网站建设 2026/10/13 18:05:28

蓝屏分析工具原理:从dump文件解析到驱动定位与自动化排查

简介&#xff1a;Bluescreenview蓝屏分析工具资源包面向Windows系统维护人员、IT运维及普通用户&#xff0c;用于解析系统蓝屏时生成的DMP文件&#xff0c;快速定位错误代码、停止消息与驱动程序等崩溃线索。资源包共3个文件&#xff0c;以inscode工程配置、html页面与gitignor…

作者头像 李华
网站建设 2026/10/13 18:04:14

全转动副三自由度并联机器人设计:从自由度计算到避坑指南

简介&#xff1a;这份文档是面向机械设计、机器人工程方向学生与研发人员的毕业设计资料&#xff0c;围绕全转动副三自由度并联机器人展开&#xff0c;重点解决传统并联机构解耦困难与奇点问题。作者通过全新支链设计实现运动解耦并规避奇点&#xff0c;同时给出重复定位精度小…

作者头像 李华