☰
React入门实战:从项目创建到组件与状态管理
2026/10/20 6:14:55 网站建设 项目流程

1. 项目概述

1.1 初学React前需要想清楚的事

我接触React的时间不算早,是在用原生JavaScript写完几个不大不小的项目之后,才回头补的React。那时候的感觉是:代码重复太多,操作DOM的逻辑和业务逻辑搅在一起,数据一变就要手动更新页面上好几个地方。React给我的第一印象,不是网上说的“难”,而是“别扭”,因为这玩意儿的写法和传统思维完全不一样。

学React,“组件化”“声明式”“不可变数据”这几个词是绕不开的。一个页面不是从头到尾生成HTML,而是拆成一个个独立的组件,每个组件只管自己的一亩三分地,自己的数据自己负责,数据变了页面自动跟着变。这种开发方式,像搭积木:每个积木独立成型,拼在一起就是整个页面。

这篇文章适合三种人:刚刚学完HTML/CSS/JavaScript、正准备进军框架的新手;在Vue里泡了比较久、想横向对比一下的开发者;还有那种看教程看了好几遍、但始终没动手敲过代码的拖延症患者。文章里所有代码我都在本地跑过,踩过的坑会单独标出来,争取让你照着敲就能跑起来。

1.2 这次体验的主角:创建与组件

标题里写了两个关键词:创建和组件。创建解决的是“环境从哪来、项目怎么初始化”的问题,组件解决的是“React程序怎么组织、怎么复用”的问题。这两块是React的入门门槛,跨过去之后,后面什么Hooks、状态管理、路由都会顺畅很多。

我这边的学习路径是:先用官方脚手架建一个项目,然后逐个拆解里面的文件,搞清楚React运行时到底做了什么,再动手把默认页面清空,从零写自己的第一个组件。整个过程我不打算开视频跟课,也不打算用在线编辑器糊弄自己,直接在本地把环境搞起来,用最朴素的方式感受React的实际工作方式。

2. 为什么选React作为第一个框架

2.1 框架选型背后的大实话

很多初学者面临的第一个问题是:Vue、React、Angular到底该学哪个?我当时的判断思路是这样:先看招聘市场需求,再看生态规模,最后看学习曲线是否友好。

React的核心优势是“足够纯粹”。它本身只解决视图层的问题,官方提供的核心API非常少,class组件时代就是那十几个生命周期,后来函数组件加Hooks之后,核心概念也就是useState、useEffect这么几个。学习成本不高的原因在于,它的心智模型非常统一:状态决定视图,状态变了视图就变。你不需要理解服务端渲染、路由、构建优化这些东西,也能先把页面跑起来。

Vue的上手速度快,是因为模板语法接近HTML,但对新手来说这不一定完全是好事。写久了模板,对JavaScript本身的依赖会减少,数据响应式这一层被框架包装得太好,很多时候你根本不知道背后发生了什么。React反而不包装这些:没有双向绑定,没有指令,就是函数调用,就是JavaScript本身。这让我觉得,学React的过程中顺便能巩固JavaScript,这是最初没想到的额外收获。

2.2 React解决的核心痛点

用原生JavaScript写页面的时候,最烦的事情莫过于手动操作DOM。一个输入框的值要响应到页面上,你得先拿到元素,再监听事件,再更新另一个元素的内容。如果页面上有十个地方依赖同一个数据,那就有十处更新逻辑要维护。我写过最痛苦的一段代码,就是一个筛选功能同时同步三个下拉框、一个列表和一段统计文字,改一处漏两处,写到最后自己都不敢改。

React把这个过程打包成了简单两步:声明状态,然后在JSX里描述“状态长什么样”。状态变了,组件重新执行函数,React内部自动算出前后两次的差异,只更新需要动的那部分DOM。你不用再关心“哪里要改、改成什么”,你只需要回答“数据是什么”。这个转变让我写代码的心态变化很大,整个人的关注点从“操作步骤”变成了“描述结果”,用声明式的思路替代原来的命令式操作。

2.3 React背后的虚拟DOM原理速览

新手第一次听说虚拟DOM,容易理解成“不直接操作真实DOM了”,其实这个说法不完全对。React确实不直接操作真实DOM,但它维护的是一棵用JavaScript对象表示的虚拟节点树。每次组件状态更新,React会重新生成一棵新的虚拟树,然后用diff算法对比新旧两棵树的差异,最后把差异部分批量提交给真实DOM。

用大白话解释:真实DOM像是写在黑板上的内容,每次擦掉重写成本很高;虚拟DOM是脑子里先构思好要改成什么样,对比出区别后再上黑板只改不同的地方。实际开发中,这个性能提升主要体现在高频率更新场景,比如输入框、列表排序、实时刷新这类操作。初学者不必深挖diff算法细节,但要知道为什么React能高效更新界面,这对手写组件时的性能思路有帮助。

提示:虚拟DOM并不是所有场景都比直接操作DOM快,React的收益更多来自开发效率和跨平台能力。真正想学性能优化,那是另外一个深水区了。

3. 本地环境准备与项目创建全流程

3.1 Node.js和包管理工具的正确选择

React项目跑在Node.js环境上,这一步绕不过去。安装Node.js的时候有一个关键选择:装LTS版本还是Current版本。我建议装LTS(长期支持版),稳定性优先。某些React相关工具链对新版本Node支持不及时,装Current版本容易踩到兼容性坑,我身边就有同学因为Node版本太高导致Vite启动报错,最后老老实实换回LTS才解决。

包管理工具我在npm、yarn、pnpm之间犹豫过一阵。npm是Node自带的老牌工具,优势是零额外安装、兼容性最好;yarn当年靠并行下载速度快出名,但现在npm也追得差不多;pnpm是后起之秀,硬链接机制省磁盘空间,安装速度在项目多了以后优势明显。新手阶段我建议直接用npm,少一个变量就少一个坑。等以后你同时维护的项目多到磁盘吃紧,再切pnpm也不迟。

安装完之后,在终端里运行node -v和npm -v,如果能正常打印版本号,环境就基本就绪了。这一步最常见的坑是终端提示“node不是内部或外部命令”,绝大多数情况是安装时没勾选“Add to PATH”,要么重装,要么手动把Node的安装路径加到系统环境变量里。

3.2 用Vite初始化React项目

创建React项目的方式,最传统的是官方脚手架Create React App,缩写是CRA。但CRA现在官方都推荐新项目改用Vite了。Vite的最大优势是启动速度和热更新速度非常快,因为它利用浏览器原生ES模块,开发时无需打包,省去了Webpack那种一启动就等几十秒的煎熬。我用的就是Vite。

在终端里执行一行命令,项目就创建出来:

npm create vite@latest my-react-app -- --template react

命令里的my-react-app是项目名称,模板指定为react。运行过程中它可能会问你是否安装create-vite这个包,输入y回车就行。创建过程很快,十几秒就能完成。之所以在create前面用npm create而不是npm install,是因为npm create是npm 7以上版本提供的快捷方式,会自动下载对应工具并执行初始化。

创建完成后按顺序执行三条命令:

cd my-react-app npm install npm run dev

npm install会按照package.json里的依赖清单把React、ReactDOM这些包全都装到本地node_modules目录,正常情况下几分钟内完成。npm run dev启动开发服务器后,终端会打印一个本地访问地址,默认是http://localhost:5173,用浏览器打开能看到React官方默认页面,说明环境搭建成功。

3.3 初始化后的目录结构逐个拆

建好项目不要急着写代码,先把目录结构搞清楚,不然找不到文件在哪,体验会很糟。Vite创建的React项目结构不算复杂,最核心的几个文件我都过一遍:

  • index.html:整个应用的HTML入口。Vite把index.html放在根目录而不是public目录,这和传统后端的模板结构不太一样。浏览器打开页面时加载的就是这个文件,里面会有一段 ,React最终要把内容挂载到这个div里。
  • src/main.jsx:JavaScript入口文件。它做的事情主要是把React应用挂载到页面,核心代码是ReactDOM.createRoot(document.getElementById('root')).render( )。
  • src/App.jsx:根组件文件。一个React应用本质上就是一棵组件树,App.jsx是这棵树的根节点。
  • src/App.css和src/index.css:样式文件,分别对应组件样式和全局样式。
  • vite.config.js:Vite配置文件,新手阶段基本不需要动。

这里面main.jsx值得多看两眼:

import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.jsx' ReactDOM.createRoot(document.getElementById('root')).render( <React.StrictMode> <App /> </React.StrictMode> )

createRoot是React 18版本引入的新API,作用是创建React根节点,然后把组件树渲染进真实DOM。React.StrictMode是严格模式,它不会渲染任何可见内容,只会在开发环境下做额外的检查,比如帮你会发现组件里潜在的副作用问题。我刚接触时总以为StrictMode是多余的,后来才知道它对养成良好编码习惯有帮助,生产环境构建时它不会产生任何额外代码。

注意:开发环境热更新失效、页面刷新报错,八成是node_modules依赖损坏,先删掉node_modules和package-lock.json,再重新npm install,概率能解决。

3.4 清空默认页面,开始第一次极简渲染

Vite的默认页面是官方Demo,里面引用了React的Logo,还有一些示例组件,放到项目里当模板没问题,但我要学习,就需要保留一个干净起点。我把App.jsx里面的内容整个清空,替换成最简单的代码:

function App() { return ( <div> <h1>Hello React</h1> <p>这是我的第一个React组件</p> </div> ) } export default App

保存之后,浏览器页面会自动更新。现在不管你怎么刷新,看到的都是这两行内容。这个简单的页面背后已经发生了很多事:React把这段看起来像HTML的代码解析成JavaScript对象,再把对象渲染成真实DOM。这就是组件的最基本形态:一个返回JSX的函数。

3.5 创建过程中踩过的坑与排查方法

整个创建流程我遇到过三个典型问题。第一个,终端执行npm create时长时间没反应,最后报网络错误。排查后发现是网络镜像源的问题,npm默认源下载速度不稳定,换成国内镜像源后速度提升明显。设置镜像源的命令是:

npm config set registry https://registry.npmmirror.com

第二个,npm install过程特别慢,卡在某个包不动。这个其实也是网络问题,和上面同一个解决方案。第三个,浏览器打开后页面白屏,控制台报错提示找不到某个依赖。这个一般是依赖没装全,把node_modules删除后重装,基本能解决。

经验之谈:新项目创建后,第一时间看package.json里的依赖版本,如果发现包版本过老或者过新,直接改版本号再npm install。React 18之前的API和18有差异,网上搜到的旧教程代码可能直接跑不起来,注意区分。

3.6 项目文件的组织方式

项目创建好之后,文件组织方式会影响后续的开发体验。我习惯的规划是:所有组件都放进src/components目录,每个组件一个文件夹,里面放组件文件、样式文件和测试文件。比如建一个Button组件,就创建一个src/components/Button目录,里面放Button.jsx和Button.css。

页面级的组件放在src/pages目录,公共组件放在src/components目录,工具函数放在src/utils目录,接口请求放在src/api目录。这个分层在项目规模小的时候看不出优势,等组件多了、页面多了,没有良好组织的话,src目录会变成一个文件堆积场,找一个组件要翻半天目录树。

4. 深入理解组件:函数组件与类组件

4.1 组件到底是个什么东西

组件这个词听起来抽象,其实它就是一段可以复用、可以组合的代码单元。拿看得见摸得着的东西比喻,组件就像乐高积木。每一块积木有固定形状和接口,不同积木之间通过凸起和凹槽拼插组合。React组件也一样,有自己的结构、自己的功能,还可以从外部接收参数,用来控制显示效果。

一个React应用的页面结构,从组件角度看就是一棵倒挂的树。根组件是整棵树的起点,它的子组件形成树枝,子组件的子组件形成更细的枝条。数据从根部往下传递,一层一层传递到叶子组件。这种做法把复杂页面拆成了许多小模块,每个模块只知道自己职责范围内的信息,不再需要统筹全局,代码可维护性就有了基础。

4.2 函数组件:React的入门第一步

函数组件就是普通的JavaScript函数,按约定,函数名首字母大写,返回值是JSX结构。React 18时代,函数组件已经成为主力写法,官方文档也推荐用函数组件加Hooks代替类组件。为什么函数组件受欢迎?因为它足够简单——不需要记忆生命周期函数的执行时机,不需要处理this指向问题,就是”函数进来、JSX出去“。

一个最常见的函数组件长这样:

function UserCard(props) { return ( <div className="user-card"> <h2>{props.name}</h2> <p>职业:{props.job}</p> </div> ) }

这个组件接收一个props对象作为参数,name和job是外部传入的数据,组件内部只负责把数据显示出来。它的关注点非常单纯,没有任何多余逻辑。props里的内容你可以理解成外部递给组件的一张纸条,组件拿到纸条按上面的信息来渲染内容。这种数据流是单向的,从父组件流向子组件,子组件不能反向修改父组件的数据,这在调试时非常舒服——你可以确定数据的来源,顺着组件树往上就能找到。

第一次看完这个函数组件,我最大的感慨是:这不就是个函数吗?原来框架没我想的那么玄乎。React的哲学之一就是“UI是函数的结果”,给相同的输入,一定能得到相同的输出,这让代码的行为变得可预测。

4.3 类组件:虽然旧,但能看懂老代码

函数组件简单,但React生态里还有大量老项目是用类组件写的。我刚学的时候不太理解为什么要有类组件这种写法,直到读开源项目代码时才明白,理解类组件是为了能读懂存量代码。

类组件的基本形态:

import React, { Component } from 'react' class UserCard extends Component { render() { return ( <div className="user-card"> <h2>{this.props.name}</h2> <p>职业:{this.props.job}</p> </div> ) } }

和函数组件一比,差异很明显。类组件需要继承React的Component类,必须实现render方法,访问props的时候用的是this.props。类组件里可以使用this.setState来更新内部状态,可以声明生命周期方法,结构更复杂但能力更多。

实际生产中,React 18都出来了,新项目再用类组件属实没必要。函数组件加Hooks能完成类组件的所有功能,代码还更简洁。但面试会问类组件和函数组件的区别,接手老项目也难免碰到,了解它的原理不是为了在新项目里用它,是为了应急。

4.4 组件通信:props与回调函数

单组件只是入门,页面是多个组件协作的结果。组件之间的通信,最常见的方式是父组件通过props向子组件传数据;反过来,子组件要向父组件传数据,传的是回调函数。

举一个实际场景:父组件有一个用户列表,子组件是列表里的每一项,点击子组件的“删除”按钮,要告诉父组件“这一项要被删除”。实现方式是这样:

// 父组件 function UserList() { const users = [ { id: 1, name: '张三' }, { id: 2, name: '李四' } ] const handleDelete = (id) => { console.log('删除用户:', id) } return ( <div> {users.map(user => ( <UserItem key={user.id} user={user} onDelete={handleDelete} /> ))} </div> ) } // 子组件 function UserItem({ user, onDelete }) { return ( <div> <span>{user.name}</span> <button onClick={() => onDelete(user.id)}>删除</button> </div> ) }

子组件的onDelete属性是父组件传下来的函数,子组件不调用它也永远不会执行,只有点击按钮时才触发。这个设计很巧妙——子组件并不知道删除的具体逻辑是什么,它只负责把消息传递出去,父组件怎么处理完全是父组件的事。这就是单向数据流带来的清晰职责划分。

4.5 组件的渲染流程与更新机制

一个React组件从诞生到消失,会经历挂载、更新、卸载三个阶段。挂载阶段执行函数体,返回JSX并渲染到页面上;更新阶段发生在props或state变化时,组件函数重新执行,React对比新旧虚拟DOM后更新真实界面;卸载阶段发生在组件从组件树中被移除时。

函数组件没有类组件那些生命周期方法,React 18用Hooks来替代,比如useEffect可以在组件挂载后、更新后、卸载前各执行一段逻辑。写函数组件时,心里要装着render这个动作代表什么:每次render,函数体从头到尾执行一遍,新产生的返回值会用于对比更新。所以不要在函数体里做昂贵的计算或者发起网络请求——那会导致每次更新都重复执行。

4.6 受控组件与非受控组件:表单处理的基本功

表单是Web应用里躲不开的场景。React对表单的处理跟原生HTML有很大不同:原生表单有自己的内部状态,用户输入的内容存在DOM的value属性里;React则推荐用状态来控制输入框的值,这种组件叫受控组件。

function LoginForm() { const [username, setUsername] = useState('') const handleChange = (event) => { setUsername(event.target.value) } return ( <input type="text" value={username} onChange={handleChange} /> ) }

输入的value由username状态控制,用户每一次键入,触发onChange,通过setUsername更新状态,React重新渲染,输入框显示新值。状态成为唯一数据源,任何时候你要拿到输入框的值,直接读username就行,不用再通过查找DOM元素的方式拿。非受控组件则是不控制值属性,用ref直接读取DOM的value,适用于不需要实时干预输入内容的场景,如表单提交时才取值的简单表单。受控组件的优势在动态校验、联动查询、条件禁用时能体现得很明显。

5. 状态管理:React心智模型的核心

5.1 useState:函数组件的第一口甜头

组件只有静态展示是不够的,真实页面需要交互,交互就需要状态。React的useState是函数组件中最基础的Hooks,用来让组件“记住”一些数据。以计数器为例子:

import { useState } from 'react' function Counter() { const [count, setCount] = useState(0) const increment = () => { setCount(count + 1) } return ( <div> <p>当前计数:{count}</p> <button onClick={increment}>加一</button> </div> ) }

useState返回两个值:当前状态值和更新状态的函数。每次调用setCount,组件重新渲染,count变量更新为最新的值。用数组解构的写法,说明React官方希望你在需要多个状态时,多次调用useState把它们分开管理,而不是在一个大对象里集中存放。

一个容易忽略的细节:setCount传入的是新值,React会把这个新值作为下一次渲染的count。如果你需要基于旧值计算新值,直接用count在闭包里取值,在某些情况下可能会拿到过期数据。以防这种情况,React允许传入一个函数:

setCount(prevCount => prevCount + 1)

函数参数里的prevCount始终是当前最新值,适合连续更新、异步更新这类场景。我在写计数器连带双击事件时踩过这个坑,第一次双击结果只加了1,原因是两次点击用的都是同一个闭包里的旧值。

5.2 状态不可变更新:为什么不能直接修改变量

React有一条铁律:更新状态时不要直接修改它,而是创建新数据替代。比如有一个数组状态:

const [list, setList] = useState([1, 2, 3]) // 错误写法 list.push(4) setList(list) // 正确写法 setList([...list, 4])

错误写法的问题在于,list被push修改后,和之前的list内存引用相同,React比较旧状态和新状态时会认为它们没有变化,不会触发组件更新,界面就不会刷新。正确写法会创建一个新数组,React才能检测到变化。

这条规则背后的原因跟React的更新机制有关。React判断状态是否变化用的是Object.is比较,创建新引用才能让比较结果为false,组件才能重新渲染。这个细节对性能也有影响:一旦遵守不可变更新,React可以做更精细的更新追踪。新手时期我也觉得这是多此一举,直到在列表页面遇到过数据状态改了但界面怎么都不刷新的问题,才意识到不可变更新不是风格偏好,而是React的底层要求。

5.3 状态提升:多个组件共享同一个数据

页面里如果有两个组件需要显示同一份数据怎么办?React给出的方案是状态提升。把数据放在两个组件的共同父组件里,通过props传给两个子组件。输入框组件负责修改状态,展示组件负责读取状态,状态在父组件中保存,两个子组件都通过props获取。

function Parent() { const [keyword, setKeyword] = useState('') return ( <div> <SearchInput value={keyword} onChange={setKeyword} /> <SearchResult keyword={keyword} /> </div> ) }

状态提升的好处是非常直白地展示数据流:状态在哪,谁改它,谁读它,一眼便知。等以后你发现props层层传递很烦,自然会去找Context或状态管理库,但那时候你已经理解了它们的本质——无非是把状态提升到更高层级,或者提升到组件树之外,让更多组件可以访问。

5.4 使用状态时常见的三个错误

第一,在useState里放入对象/数组作为初始值时,每次渲染都会重新创建初始值,期望只在初次创建一次?实际上不会,useState的初始化值只在组件首次渲染时生效,后续渲染时这段初始化代码会被忽略,不会反复执行,这个不用过度担心。

第二,组件卸载后仍然调用setState,浏览器控制台会报“Can't perform a React state update on an unmounted component”。实际场景比如组件里发起了一个网络请求,返回之前用户切走了页面。React 18之后这个警告有所调整,但逻辑上仍然要尽量避免在卸载后更新状态,常见做法是利用useEffect的清理函数取消请求。

第三,把多次相关状态拆成多个useState调用,导致setA、setB、setC连写三行。如果它们本来就属于同一份业务数据,用useReducer或useState保存单个对象可能更合适。状态拆分的粒度没有标准答案,但原则是“相关一起变的数据放一起管理”。

5.5 Hooks规则里不能破坏的两条底线

React Hooks有两个规则,违反任意一条都不会报语法错误,但运行时会有严重问题。第一条:只在函数组件顶层调用Hooks,不要在循环、条件语句或嵌套函数里调用。原因很简单——React依赖Hooks的调用顺序来保持状态与组件实例的关联,每次渲染调用顺序必须保持一致,否则状态会错乱。第二条:只能在React函数组件或自定义Hooks中调用Hooks,不要在普通JavaScript函数里调用。

实际开发中,条件调用useState的典型错误场景是这样:组件里需要根据某个布尔值决定是否初始化一个状态。直觉写法是if (visible) { useState(...) }。这段代码在visible为true时调用Hooks、false时不调用,下一次渲染顺序就乱了。解决办法是把条件判断移到组件内部,始终无条件调用useState,再根据条件决定如何使用它。

6. 关键差异意识:JSX不是HTML

6.1 JSX与HTML的核心区别

用React写页面,最直观的差异就是JSX。JSX是JavaScript的语法扩展,看起来像HTML,但不完全是HTML。第一次写React时,我把class直接写进JSX里,页面样式完全没有生效,控制台还报了错误。原因很简单:JSX里不能用class,要用className。

为什么不能用class?因为class是JavaScript的保留字。JSX最终会被转译成React.createElement调用,className在其中作为props传给React,最终映射为DOM的class属性。同理,htmlFor替代label标签里的for。

风格上还有一个习惯:JSX标签属性命名使用驼峰式。onclick变成onClick,tabindex变成tabIndex,onchange变成onChange。导致这些命名的根本原因是JSX对象的属性风格更接近JavaScript而不是HTML。

6.2 JSX里怎么渲染数据:花括号的使用

JSX的表达式插值用单层花括号,里面放的是JavaScript表达式,可以是一个变量、一次三元判断、一个函数调用。比如:

const userName = '小明' const isVip = true return ( <div> <p>用户名:{userName}</p> <p>会员状态:{isVip ? 'VIP用户' : '普通用户'}</p> </div> )

注意花括号里不能写完整的JavaScript语句,比如if、for这类。如果需要条件渲染,有三元表达式或逻辑与。需要循环渲染,用数组的map方法。JSX表达式的原则是:任何可以在等号右边出现的内容都可以放进去。

列表渲染是新手必然会碰到的场景:

function List() { const items = ['苹果', '香蕉', '橙子'] return ( <ul> {items.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> ) }

每个列表项都必须有一个key属性,React用key来识别列表中的每一项,在增删排序时追踪每个元素。我刚开始注册key最省事的方式就是用index,后来才知道列表项顺序变化时,index为key会引发状态错乱。最稳妥的key是数据里天然存在的唯一id,比如用户id、订单号。只有在列表是纯静态展示、不增不改不排序时才适合用index。

6.3 条件渲染的几种常见写法

React的条件渲染,本质上还是JavaScript的条件判断。只是用不同的语法风格组合在JSX里:

// 三元表达式 {isLoggedIn ? <UserPanel /> : <LoginButton />} // 逻辑与,适合只显示、不显示两种状态 {isLoading && <div>加载中...</div>} // switch风格,通过函数返回 function getContent(status) { switch (status) { case 'loading': return <div>加载中</div> case 'error': return <div>出错啦</div> default: return <div>正常显示</div> } }

eng注意:逻辑与写法中,isLoading为false时React不会渲染任何内容;但如果isLoading是0或者空字符串,React会渲染数字0或者空字符串。所以在条件渲染时,保证条件变量是布尔值,如果是数量判断,要写成{count > 0 && ...},不要写{count && ...}。

6.4 key属性的理解和应用

前面提到列表渲染必须用key,React官方建议key应该是在数据里稳定存在的唯一标识。为什么key必须稳定?假设有一个渲染用户列表的组件,每个列表项内部还有一个输入框。如果使用index作为key,当用户列表前插入一条新数据时,React会认为原来的第一个节点变成了第二条数据,输入框会被复用,但用户输入的内容就串到了错误的位置。用唯一id作为key,React就能准确识别哪条数据去哪了,输入框跟着正确的数据走。

6.5 样式方案的几种选择

React的样式方案比原生HTML灵活,选择也多。最基础的用法是className配全局CSS,用className指定样式类名,页面效果跟原生CSS一致。复杂项目常用CSS Modules,Vite默认支持这种方案,局部作用域样式,类名编译后自动加哈希后缀,避免样式冲突。更进阶的有styled-components这种CSS-in-JS方案,把样式直接写在JavaScript文件里,动态样式特别灵活。

新手阶段我推荐CSS Modules,一次配置后续开发体验都比较顺。它解决的“样式冲突”问题是全局CSS最让人头疼的:两个不同组件里都定义了.title类,后加载的类会覆盖先加载的类,排查起来相当费劲。CSS Modules把这个痛点解决了,学习成本几乎为零,就是import样式后当对象用它的属性名。

import styles from './Card.module.css' function Card() { return <div className={styles.card}>卡片内容</div> }

7. 从零搭建第一个入门组件:完整实操演示

7.1 制作一个用户名片卡组件

掌握了前面的知识点,现在完整做一个小组件练手。需求很简单:做一个用户名片卡,显示头像、昵称、签名,点赞功能,根据点赞数量显示不同文案。拆解这个需求:这是单一组件,有状态(点赞数),有props(用户数据),有交互(点击按钮)。

先做静态结构。新建src/components/UserCard.jsx:

function UserCard({ user }) { return ( <div className="user-card"> <div className="avatar">{user.avatar}</div> <div className="info"> <h3>{user.name}</h3> <p>{user.bio}</p> </div> </div> ) } export default UserCard

user是一个包含头像、昵称、签名的数据对象。组件把这个对象拆分,各个字段渲染到对应位置。这里的props解构写法在函数组件中很常见,我第一次写总是写props.user.name,解构之后代码简洁很多。

然后在App.jsx里引入这个组件,并传入数据:

import UserCard from './components/UserCard' function App() { const user = { name: '小明', bio: '前端开发者,喜欢捣鼓新玩意', avatar: '🐯' } return <UserCard user={user} /> } export default App

数据从App传递到UserCard,UserCard不直接拥有数据,只负责渲染。数据来源集中在App里,修改数据时只需要改一处。页面刷新后,卡片就显示出来了。

7.2 加入交互逻辑:点赞功能和文案联动

静态视图完成,接下来加状态和交互。修改UserCard,加入useState:

import { useState } from 'react' function UserCard({ user }) { const [liked, setLiked] = useState(false) const [likeCount, setLikeCount] = useState(0) const handleLike = () => { setLiked(!liked) setLikeCount(prev => liked ? prev - 1 : prev + 1) } return ( <div className="user-card"> <div className="avatar">{user.avatar}</div> <div className="info"> <h3>{user.name}</h3> <p>{user.bio}</p> <button onClick={handleLike}> {liked ? '已点赞' : '点赞'} </button> <span>{likeCount > 0 ? `${likeCount} 人赞过` : '还没人点赞,抢个沙发'}</span> </div> </div> ) }

这里的关键逻辑在handleLike里。liked表示是否已点赞,likeCount是点赞总数。点击按钮时,点赞状态取反,同时根据取反后的liked值决定likeCount是加一还是减一。注意我用了函数式更新setLikeCount(prev => ...),就是为了确保在状态连续变化时始终拿的是最新值。

这套实现逻辑覆盖了React入门的核心内容:函数组件、props传参、JSX渲染、条件渲染、状态管理、事件处理。代码量不大,但每一个语法点都有实际意义,建议你亲自动手敲一遍,遇到错误就查控制台,过程比结果更有价值。

7.3 抽离子组件:卡片里的每个模块

用户名片卡已经能用了,但组件内部还可以拆。把头像、信息区、按钮分别做成小组件,结构更清晰。比如:

function Avatar({ emoji }) { return <div className="avatar">{emoji}</div> } function UserInfo({ name, bio }) { return ( <div className="info"> <h3>{name}</h3> <p>{bio}</p> </div> ) } function LikeButton({ liked, onClick }) { return ( <button onClick={onClick}> {liked ? '已点赞' : '点赞'} </button> ) }

然后UserCard组合这三个子组件。每个子组件只做一件事,测试起来非常容易,想改样式也不会误伤其他部分。子组件之间互不干扰,这正是组件化的核心价值——把一个大问题拆成若干小问题,每个小问题的解决方案都是独立的。

7.4 在这个实操里学到的东西

完成这个小组件后,我对React的几个核心概念都有了直观认识。props让我理解了怎么传数据、数据流向哪;useState让我掌握了怎么让组件“活”起来;拆分组件让我体会到了复用自己的代码。这套顺序学习下来,前面零散的JSX语法、虚拟DOM原理、组件概念都串联起来了,形成了完整的心智模型。

8. 新手高频坑点:React初学避雷指南

8.1 渲染不更新:状态改了页面没反应的查找顺序

React新手遇到最多的问题就是“改了状态页面没变”。我的排查思路可以总结成一条链:先确认setState有没有被调用,再看调用时传的是不是新引用,再看组件有没有被卸载,最后看是不是有多个组件实例导致改的不是同一个。

最常见的原因有两个。第一,直接修改了对象或数组类型的状态。前面说过React基于引用比较,直接修改相同引用不会触发渲染。解法是创建新对象再set。第二,异步场景下拿旧状态闭包设置了新状态。比如在setTimeout里用旧值加一,连续触发两次,结果只加了一次。解法是用函数式更新。

8.2 JSX语法细节:为什么我的标签对不上

JSX要求所有标签必须正确闭合和嵌套,一个标签没闭合,整个组件都会报错。常见的错误有:自闭合标签写成了

,导致React解析到末尾还在等闭合标签;注释方式不对,JSX注释用{/* ... */}而不是HTML的 ;属性值漏了花括号,导致变量名被当成字符串。

还有一个细节:JSX相邻的顶层元素必须包在一个标签里。如果你return两个相邻div,React会报“Adjacent JSX elements must be wrapped in an enclosing tag”。解法是外面包一层div,或者用Fragment空标签<></>。

8.3 组件的重新渲染:为什么子组件也跟着跑

父组件每次状态更新,都会导致子组件重新渲染,这个现象新手会困惑。比如父组件输入框每次键入都重新渲染,子组件接收一个静态props但也在跟着执行。React的默认行为是父组件render,所有子组件都跟着render,不管props有没有实际变化。这不一定带来性能问题,但如果子组件渲染开销大,就会卡顿。

解决的方案是React.memo,它包裹组件后会在props没有变化时跳过组件重新渲染。注意memo只做浅层比较,嵌套对象属性变化它感知不到,实际要结合使用场景判断是否有效。新手阶段不用纠结这个性能问题,先理解渲染机制才是重点。

8.4 useEffect里的依赖数组:写错就出大事

useEffect在函数组件里用来处理副作用,比如订阅事件、发起请求、操作外部API。它的依赖数组很容易写错。最简单的方式是每次渲染执行,不传依赖数组;传空数组,只在挂载和卸载时执行;传具体变量,变量变化时执行。

空数组依赖和React严格的规则放一起会产生一个经典场景:useEffect里要读取一个外部状态,但依赖数组是空的,React会在每次渲染都看到旧的state。新手以为useEffect只在初始化时执行一次就好了,结果发现里面读到的是第一次渲染的值。解法是把依赖加进数组,或者用ref读取最新值。这个坑我花了整整一个下午才明白,所以特别拿出来提醒一下。

9. 初学阶段的实用技巧与扩展建议

9.1 利用浏览器插件提升调试效率

开发React项目,Chrome浏览器配上React Developer Tools插件能大幅提升调试体验。装上以后,浏览器开发者工具的Components面板能直观看到组件树,点击任意组件能看到它的props和state。还有Profiler面板,用来分析组件渲染性能,找到性能瓶颈很有用。Vite项目开发模式下默认有热更新,修改代码保存即刷新页面,对快速迭代很有价值,是我最常用的工具之一。

9.2 遇到问题的排查路径

自己学习React,遇到的问题多数不是突破性的,大部分都能通过搜索解决。碰到报错先读报错信息,React的控制台报错提示已经足够具体,比如告诉我们附近缺少key、标签需要包裹等等。看不懂报错时再去搜索,搜索关键词用英文报错原文,而不是中文翻译,搜索结果质量往往高很多。

排错时还可以用一个小技巧:如果某个组件渲染结果和你预期的不一致,在render的return前加console.log打印props和state,观察每次渲染的数据变化。数据对了但页面不对,问题在JSX;JSX对但结果错,问题在逻辑;两个都对但页面还不对,多数是样式或事件处理的问题。

9.3 学完组件之后的学习路线

组件这块基础打好之后,后面的主线就是把React生态串联起来。建议按这个顺序推进:先系统过一遍useEffect、useContext、useReducer这几个Hooks;然后学React Router做页面路由;接着学fetch或axios调接口;再往后接触状态管理库,比如Zustand或Redux Toolkit;最后再看Next.js这种全栈框架。

React官方文档一直是我推荐的学习材料,里面的互动教程特别适合新手。不要只看不写,每学一个概念就找一个交互场景把它用起来,哪怕只是一个计数器、一个待办清单。我自己的经验是,光看文档产生“我懂了”的错觉太常见了,动手写一次才知道自己哪里没理解透。

9.4 和Vue的横向对比给React初学者的建议

很多从Vue过来的开发者会觉得React别扭——没有模板指令、没有双向绑定、JSX需要适应。我的经历是从React切入再了解Vue的,但身边不少朋友正好相反。就开发思维来说,Vue更偏“搭积木式的拼装”,模板上手快;React更偏“JavaScript的函数式思维”,需要你有一定的逻辑抽象能力。如果你感觉React学起来不顺,不代表你不行,只是还没适应它的思维方式。

9.5 一个值得养成的编程习惯:小步提交及时验证

我学React期间受益最大的一个习惯,就是每写十几行代码就保存一次并在浏览器里看效果。不要一口气把整个组件写完再验证,出了问题很难定位。小步修改、及时验证,问题范围自然就小了。这个方法在React这种声明式框架里尤其好用,因为你改的每一点都能立刻看到视图反馈。

最后给自己的学习过程做个总结:初学React不需要第一天就理解虚拟DOM的全部细节,也不需要死记所有Hooks API。先把“创建一个项目、写一个组件、让界面动起来”这三步做好,整个React的世界会自然而然地在你面前展开。遇到问题不逃避,动手试错就是最好的老师。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询