资讯详情

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

📅 2026/10/13 18:13:12 | 华诺云谱 👁 阅读
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 classNametitle你好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 classNamewelcome-box h2欢迎回来{username}/h2 p你有 {messageCount} 条未读消息/p /div ); }类组件长这样了解即可主要是读老代码用import { Component } from react; class WelcomeMessage extends Component { render() { const { username, messageCount } this.props; return ( div classNamewelcome-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 classNamecard div classNamecard-header{title}/div div classNamecard-body{children}/div /div ); } // 使用方传入任意结构 Card title今日推荐 ul liReact基础/li liTypeScript入门/li /ul /Cardchildren可以是任何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环境。拿到后你先做三件事删掉自带的logo和示例代码。把App组件拆出来单独建components目录放一个自写的卡片组件。打开浏览器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 typetext value{keyword} onChange{(e) setKeyword(e.target.value)} placeholder输入关键词 / button typesubmit搜索/button /form ); }表单里input的显示值由React的state控制用户输入触发onChange更新statestate更新后input重新渲染——这就是“受控组件”它的优点是真正确保了“显示在页面上的”和“存于React状态里的”永远一致不会出现DOM游离状态。几乎所有交互都是这个模式的变体状态定义在组件里事件处理器更新状态状态驱动渲染。能够清楚地按这个闭环思考React基础就算真正过关了。4.3 案例实现一个可复用的标签列表组件这里给出一个稍微综合一点的例子把props、state、children、事件结合起来function TagList({ tags, onRemove }) { if (!tags.length) { return p classNameempty暂无标签/p; } return ( ul classNametag-list {tags.map((tag) ( li key{tag.id} classNametag-item span{tag.name}/span button typebutton 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 为什么我的组件没有重新渲染排查这类问题的固定三连检查state是否真的变了。在setState前后打印值确认引用是否变化。检查是不是在错误的地方做了setState。比如在组件函数体里直接调用setState这会死循环在条件渲染的子组件里写了setState父组件重渲染时会反复触发。检查是不是传给子的props引用没变。子组件用React.memo包裹后会做浅比较不会换引用就不重渲染。还有一种隐蔽情况确实能渲染但你以为的“没渲染”其实是数据来源错了。用React DevTools看组件props和state一眼就能定位是数据本身有问题还是渲染逻辑有问题。5.2 key到底要怎么选为什么不能用indexkey是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报错类组件场景如果你还在维护类组件代码事件处理函数必须绑定thisclass 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页面跳转必备学起来很快跟着官方教程案例写两遍就能上手。代码规范与错误边界组件报错时的降级处理、代码分割、懒加载这类工程化话题。TypeScriptReact的类型推导强项目规模一大配合TS写会明显减少低级错误建议尽早引入。我不主张一口气把所有生态都学完。先把基础语法练熟再把一个自己感兴趣的小项目记账本、待办清单、个人信息页从零到一写完踩完一遍实际工程的坑再来补齐工程化知识这样的成长曲线最省力。从我的实际教学体验看绝大多数React基础阶段学不完项目的人不是因为不够聪明而是被“先学完所有概念再动手”的完美主义拖住了。概念掌握到本节程度就已经足够撑起第一个项目。请务必尽早写起来React这门技术只有亲手跑出第一个交互页面才算真正入门。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑