很多刚接触前端的朋友问我:React入门最难的是什么?我的答案可能和大部分人想的不一样——不是JSX语法,也不是组件通信,而是“把一个项目从无到有跑起来”这件事。现在的React已经不是一个普通脚本库,它需要一套工具链来编译、启动、热更新代码,如果不先把“创建项目”和“组件”这两个基础节点打通,后面的学习很容易变成纸上谈兵。
这篇文章就围绕React初学阶段最核心的两个实战点展开:怎么创建一个能跑起来的React项目,以及组件到底是什么、怎么用。内容不堆概念,每一步都可以直接照着做,适合刚开始接触React、被各种教程绕晕的初学者,也适合想重新梳理基础的前端同学。
1. 初学React的第一课:别急着写代码,先把“创建项目”这件事想清楚
1.1 为什么React项目不像HTML那样双击就能打开
你在网上看到的很多React教学视频,会直接打开一个线上代码编辑器,输入几行组件代码,页面就出来了。但真正切换到本地开发时,很多人会懵:我明明写了一个.jsx文件,为什么浏览器打开后全是乱码?因为这些代码浏览器根本不认识。
浏览器能直接执行的只有原生的JavaScript、HTML、CSS。React组件里写的JSX,比如<div className="app">,本质上是JavaScript的语法糖,需要被编译成React.createElement调用后,浏览器才能识别。这一步通常由构建工具在开发服务器里完成。同时,一个现代前端项目还要处理模块依赖、资源加载、热更新这些事,所以你不能像打开一个静态网页那样直接双击文件,必须先启动一个开发服务器,让它在后台帮你完成编译和转发。
我习惯把这件事类比成做菜:HTML就像买来的即食沙拉,撕开就能吃;React项目则是一堆食材和调料,你得先开火、洗菜、切菜,最后装盘上桌。初始化的过程就是备菜,看起来麻烦,但有了这套流程,后续每次改动都能自动上桌,不需要你手动把整桌菜重做一遍。
1.2 工具链选择:Vite还是Create React App
既然需要一个构建工具,那到底选哪个?这个问题几乎每个初学者都会问。我的建议非常明确:现在新项目直接用Vite,不要再用Create React App。
不是说Create React App不能用,而是它已经谈不上适合新手。当年它的最大价值是零配置起步,但这些年依赖版本越堆越重,冷启动动不动要等几十秒甚至几分钟,而且官方已经不再鼓励新项目使用它。相比之下,Vite有几个很直观的优势:
- 启动速度快:Vite开发环境按需编译,浏览器请求哪个模块才编译哪个模块,首次启动普遍在1-2秒内。
- 依赖处理简单:依赖预构建机制帮你在开发时省掉大量构建时间。
- 模板更灵活:创建项目时可以选React、Vue、Svelte,也可以选带TypeScript的版本,后续写起来更舒服。
有人担心Vite是不是太新、不稳定。放心,Vite现在的生态已经很成熟,React官方文档也推荐使用Vite或者其他现代工具链。初学者最开始要的不是把构建原理研究透,而是用最快的速度看到页面效果,Vite正好是那个让你少踩坑的选择。
1.3 环境准备:Node.js版本与包管理器
不管选哪个工具,你电脑里都得先有Node.js。Node.js是什么?可以简单理解成浏览器外运行JavaScript的“底座”,Vite和React项目都在它上面运行。打开终端(Windows用命令行窗口,macOS用终端),输入node -v,如果看到一个类似v20.11.0的版本号,说明已经安装。如果提示找不到命令,就去Node.js官网下载LTS版本,长期支持版足以应对绝大多数场景。
版本最好别太老,Vite 5以上版本要求Node.js 18或更新的版本。我之前遇到过有朋友还在用Node 14,跑npm run dev直接报错误,查了一圈发现就是版本问题。升级Node之后,项目马上正常。
包管理器方面,Vite脚手架默认用npm,这也是所有React初学者最直接的使用方式。你如果熟悉yarn或pnpm,也可以用,但刚开始建议盯准一个工具,不要在一个项目里npm install和pnpm install混着用,否则极容易出现依赖版本对不上、node_modules安装混乱的问题。我见过太多新人项目装一半卡住,然后三个包管理器来回试,最后整个依赖目录面目全非。
2. 从零搭建一个React开发环境(实操步骤)
2.1 用Vite创建项目:一条命令背后的原理
环境准备好了,接下来就动手。我建议在一个专门放练习项目的目录下执行创建命令。比如你想把项目建在D:\react-learn下,就先cd到那个目录,然后运行:
npm create vite@latest my-react-app -- --template react这条命令中的my-react-app是项目文件夹名称,可以随便取。执行后,Vite会问你是否安装依赖,确认后它就自动开始下载React相关的基础包。
我建议新手先试试带TypeScript还是不带?不用纠结。如果你对TypeScript还不太熟,先用纯JavaScript版本就够了,后续想升级类型系统,再看官方文档即可。命令末尾的--template react对应的是纯JavaScript的React模板,如果你想用TypeScript,可以改成react-ts。
命令跑完后,你会看到类似提示,让你执行:
cd my-react-app npm install npm run dev这里npm install是把项目依赖全部装进本地node_modules目录。你可能会想,创建的时候不是已经下载依赖了吗?其实没有,Vite创建的项目只是生成了基础骨架和package.json清单,真正的大批依赖要在这个步骤下载安装。装完之后,运行npm run dev,终端会显示一个本地地址,比如http://localhost:5173。按住Ctrl点击这个地址,浏览器就能看到Vite默认的React欢迎页。
2.2 项目结构拆解:那些文件夹到底是干嘛的
项目成功跑起来之后,很多新人的第一反应是:这么多文件,我该动哪个?我见过有人直接打开src/main.jsx,一通乱改,最后页面白了。先别急,我们花两分钟把目录过一遍。
根目录下的index.html是整张页面的“容器”,Vite会把React代码最终编译后塞进这个HTML里。打开它,你会看到一段代码:
<div id="root"></div> <script type="module" src="/src/main.jsx"></script><div id="root">就是React挂载的根节点,main.jsx是入口文件。如果你以后看到页面打不开,第一件事可以确认这里的script路径是否正确。
src目录才是你真正写代码的地方。main.jsx负责引入React核心库并执行渲染逻辑,类似这样:
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> )App.jsx则是一个典型的自定义组件,默认渲染初始化页面的大标题和Logo。src/assets通常放图片、样式等静态资源。src/index.css是全局样式,App.css是组件样式。根目录的package.json记录了当前项目的依赖和脚本,比如dev用于启动开发环境,build用于打包上线。
初学者常见误区是直接去改index.html里的内容和标题,想着把页面文字改掉。这当然能改,但如果要在React页面里展示动态内容,重点应该放在组件文件,而不是手动改HTML。HTML只是空壳,React组件才是血肉。
2.3 启动项目与第一个页面改动
项目跑起来之后,我们来做一个最快能看到效果的操作:打开src/App.jsx,把默认的标题文字改成一句话,比如我的第一个React页面,保存。你会发现浏览器里的内容瞬间变了,不需要刷新,这个过程就是热更新(Hot Module Replacement)。
热更新是Vite这类工具的巨大优势。它能在你保存文件后,把修改过的模块单独替换,而不是整个页面重新加载。某种意义上它比浏览器刷新更方便,因为它保留了你正在编辑时的大部分渲染状态。
我第一次接触Vite时,最惊喜的就是这点:保存代码后几乎感觉不到延迟,页面就变了。而用Create React App时,保存后通常要等一两秒甚至更久,大项目里差距更明显。所以如果你卡在学习进度上,很大的烦恼可能就是“改一行代码要等半天页面刷新”,换成Vite后这种挫败感会少很多。
现在你已经能从零创建项目,并成功看到自己的改动出现在页面上。这一步比背诵10个React概念都有用得多,因为你对“React项目工作流程”有了真实体感。接下来,我们进入真正的核心:组件。
3. 组件:React世界的“积木”
3.1 函数组件与类组件:为什么现在推荐函数组件
组件是React的核心概念,用一句话概括就是:页面上的每个区域都可以拆成独立的、可复用的代码片段,这个片段就是组件。比如一个页面可以拆成头部组件、导航组件、卡片列表组件、底部组件,每个组件只管自己的渲染逻辑。
React中写组件有两种老派和现代的方式。早期流行类组件,比如:
import React, { Component } from 'react' class Welcome extends Component { render() { return <h1>Hello, {this.props.name}</h1> } } export default Welcome现在绝大多数新项目都用函数组件:
function Welcome(props) { return <h1>Hello, {props.name}</h1> } export default Welcome为什么函数组件成了主流?一方面是代码更短、更好理解,没有render方法,也不用考虑this指向问题;另一方面是React Hooks的出现,让函数组件也能管理状态和副作用。Hooks出现之前,函数组件只能做纯展示,不能拥有内部状态,所以大家都被逼着写类组件。但现在有了useState、useEffect这些Hooks,函数组件的能力已经完整覆盖类组件,而且心智负担更小。
我见过很多新手被类组件里的构造函数和this.setState绕晕,写完发现报“Cannot read properties of undefined”,半天找不到原因。函数组件没有这些坑,所以除非你在维护一个老项目,否则初学阶段完全不用碰类组件。
3.2 JSX语法:在JavaScript里写HTML是怎么做到的
函数组件里返回的那段看起来像HTML的东西,就是JSX。它最大的特点就是允许你“在JavaScript里写类似HTML的结构”。不过JSX不是把HTML原样搬进来,有几个细节需要注意。
第一,给元素加类名不能用class,要用className。因为class是JavaScript的保留字,React内部会用className这个属性来映射你预期的样式类。如果你手惯写class,页面样式往往不会生效,控制台还会给你警告。
第二,组件里要表达动态内容,必须用花括号{}包裹JavaScript表达式,比如:
const name = '小明' return <div>你好,{name}</div>花括号里的内容会被当作JavaScript求值,这就是JSX的灵活性所在。你可以直接在花括号里调用函数、做运算、访问对象属性。
第三,JSX不能直接返回多个并列的根元素。你可能会想:
return ( <h1>标题</h1> <p>内容</p> )这样会报错,因为JSX要求最外层只有一个根节点。解决办法是包一个<div>,或者用<Fragment>(简写为<>)包裹。推荐用Fragment,因为它不会额外增加真实的DOM节点,避免布局受影响。
JSX底层其实会被编译成React.createElement调用,所以你在JSX里写的一切最终都变成JavaScript对象。这也是为什么React的渲染逻辑可以很灵活:组件本质上是返回“UI描述对象”的JavaScript函数。
3.3 Props与State:数据流动的两种状态
了解了组件长什么样之后,接下来最核心的问题就是:组件的数据从哪来?怎么变化?React里有两条核心线:Props(属性)和State(状态)。
Props是父组件传给子组件的数据。它就像你在函数里传入的参数,子组件通过这些参数决定自己渲染什么。Props是只读的,子组件不能直接修改Props,只能通过父组件传新值来更新。这保证了数据的流动方向是单向的:从父到子。
State是组件自己内部的数据,变化会触发组件重新渲染。在函数组件里,我们用useState这个Hook来管理状态:
import { useState } from 'react' function Counter() { const [count, setCount] = useState(0) return ( <div> <span>{count}</span> <button onClick={() => setCount(count + 1)}>增加</button> </div> ) }这里useState(0)定义了一个初始值为0的状态,返回一个数组,第一个值是当前状态,第二个值是更新这个状态的函数。点击按钮时调用setCount(count + 1),React知道这个组件的状态改变了,就会自动重新执行这个函数组件,把新的count渲染到界面上。
我见过初学者直接写count = count + 1,页面却纹丝不动。原因很简单:直接修改普通变量不会通知React重新渲染,你必须通过setCount告诉React“这个组件该更新了”。这条规则不弄清楚,后面的所有状态管理都会一头雾水。
Props和State的关系可以用一个生活场景来理解:State是组件自己的“内部习惯”,组件内部自己改动;Props是别人给你定的“外部约定”,你只能读取和执行,不能擅自修改。初学阶段把这个边界关系弄清楚,后面学自定义Hook、Context都会顺很多。
4. 组件间通信与组合方式(初学必踩的坑)
4.1 父子组件传参:Props的完整流程
单个组件毕竟是孤立的,真实页面需要多个组件协作。最基础的通信就是父子传参,通过Props完成。我举个例子,父组件里有一个用户信息对象,想把对象的name传给子组件展示:
function UserCard({ user }) { return ( <div> <h2>{user.name}</h2> <p>{user.major}</p> </div> ) } function App() { const user = { name: 'A同学', major: '软件工程' } return <UserCard user={user} /> }这里App是父组件,UserCard是子组件。父组件通过user={user}把对象传下去,子组件在函数参数中接收props,再用结构赋值直接取user字段。新手容易陷入一个误区:觉得子组件可以随便改这个传进来的对象。我之前也犯过这个错,直接在子组件里user.name = 'B同学',结果React状态管理错乱,页面数据不同步。记住,Props是只读的,想修改数据,必须由父组件通过State管理和更新。
还有一个细节:Props里有个特殊字段children,它表示组件标签里的内容。比如:
<Card>我是卡片内容</Card>Card组件的内部就能通过props.children拿到“我是卡片内容”。这个设计非常适合做布局组件,比如List、Card、Modal这类可复用容器,子元素不需要提前知道是什么,只要往槽位里丢就行。
4.2 状态提升:兄弟组件怎么共享数据
父子通信只能解决“上对下”,那兄弟组件之间怎么共享数据呢?初学时最常见的问题就是:两个兄弟组件,一个输入内容,一个要展示内容,怎么把输入框的数据传给另一个组件?
React的推荐方式是“状态提升”:把需要共享的数据放到它们的共同父组件里,作为State统一管理,再通过Props分别传给两个子组件,这样就实现了共享。比如一个简易的个人信息输入页面:
function InputArea({ value, onChange }) { return <input value={value} onChange={(e) => onChange(e.target.value)} /> } function DisplayArea({ value }) { return <p>你输入的内容是:{value}</p> } function Parent() { const [inputValue, setInputValue] = useState('') return ( <div> <InputArea value={inputValue} onChange={setInputValue} /> <DisplayArea value={inputValue} /> </div> ) }我把inputValue状态放在Parent组件里,输入框通过onChange回调把新值传给父组件,父组件更新状态后,再通过Props传给展示组件。这样无论兄弟组件有多少个,所有数据更新都集中在一个父状态上。
这个模式理解透了,你会发现React的通信思路其实非常朴素。它并不像某些框架那样提供大量全局通信工具,而是鼓励你尽量把状态放在离数据最近的那个父组件里,通过Props的单向数据流维护可预期性。这个设计在项目变复杂后,反而能减少代码被乱改的风险。
4.3 组件的渲染与key属性陷阱
组件数据发生变化后,React需要一个高效的方式去更新页面,而不是把整棵DOM树都重建。这就涉及到列表渲染和key属性。
假设我们要渲染一个名字列表:
const names = ['张三', '李四', '王五'] names.map((item, index) => <p key={index}>{item}</p>)这里如果只用index作为key,初看没错,但一旦列表内容发生增删或排序,就可能出现渲染状态错乱。比如列表项内部有一个Checkbox,删除中间一项后,后面的组件状态会被错误复用。
我给你讲个真实场景。某个新人写了一个任务列表,每一项都有一个编辑框。运行时,他删除第一项后,第二项编辑框里居然自动出现了刚刚删掉的文字。排查半天才发现,问题就出在用index当key。因为React是靠key来识别组件实例的,索引变了,React分不清哪个组件是哪个,复用了错误的实例状态。
正确的做法是使用唯一且稳定的标识,比如数据的id字段:
names.map((item) => <p key={item.id}>{item.name}</p>)如果数据本身没有id,可以用拼接出来的不可变字符串,但尽量不要用动态索引。这个细节在初学阶段很容易被忽略,等到了中大型项目,它往往是各种奇怪bug的温床。
5. 常见问题与排查技巧实录(初学者速查)
5.1 启动报错“vite不是内部命令”怎么办
这是新手问得最多的问题。现象是运行npm run dev后提示'vite' 不是内部或外部命令,或者command not found。
原因大概率是依赖没有正确安装。很多人在npm install执行到一半时按了Ctrl+C,或者网速太慢,导致依赖不全。解决办法很简单:再跑一次npm install,确保流程结束。如果还是不行,可以把整个node_modules目录删除,重新安装:
rm -rf node_modules npm install还有一个隐蔽问题:在项目目录下没有package.json文件,直接执行npm run dev。建议先用ls或dir确认当前目录结构。有时候你在别的目录下打开了终端,Vite自然找不到对应命令。
顺带提醒,新手最好保持终端路径和项目路径一致,不要开两个终端一个在项目根目录一个在乱七八糟的路径下,查找问题容易更乱。
5.2 页面白屏但控制台不报错
这个现象非常磨人,页面打开一片空白,控制台也没有红色报错,新手往往无从下手。根据我的经验,大概率出在入口渲染这一步。
首先检查src/main.jsx里createRoot的挂载节点是不是document.getElementById('root'),同时确认index.html里确实存在id="root"的DOM节点。名字稍微不一致,比如root写成了roo,就会静默失败,页面没有任何内容。
另外,看下main.jsx是否引入了App组件,以及App.jsx是否有语法错误。检查JSX时尤其要注意闭合标签是否完整。有些语法错误在控制台不会第一时间弹出,而是被Vite的开发服务吞掉。最简单的排查方法是保存文件后看页面有没有热更新提示,如果没反应,就去浏览器开发者工具的“Console”面板里翻一翻更详细的信息。
5.3 组件不更新:改state却看不到变化
这也是初学者最容易崩溃的问题之一。我见过有人写了这样的代码:
const [list, setList] = useState(['苹果', '香蕉']) list.push('橘子') setList(list)点击按钮后发现页面没有反应。原因是React的setState判断更新时,会对比新旧状态是否相同。这里的list.push修改的是原数组引用,引用没变,所以React认为数据没变,就不重新渲染。
正确做法是创建一个新数组,而不是修改原数组:
setList([...list, '橘子'])这里用展开运算符...把原数组元素复制到一个新数组里,再追加新元素。类似地,修改对象时也不要直接obj.key = value,而应该用{...obj, key: value}创建一个新的对象。这个“不可变更新”的思想,是React状态管理里非常核心的习惯。早期不养成这个习惯,后面用Redux、Zustand时也会频繁碰壁。
为了方便你快速定位,我把这几个高频问题整理成一张表:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
vite命令找不到 | 依赖未安装或路径错误 | 重跑npm install,确认在项目根目录 |
| 白屏但无报错 | 挂载节点id不匹配,或JSX闭合错误 | 检查index.html与main.jsx的root对应关系 |
| 修改State后页面没反应 | 直接修改了原数组/原对象 | 用展开运算符创建新引用,再setState |
| 列表删除后状态错乱 | 使用index作为key | 改用唯一id作为key |
| npm install下载慢 | 网络原因 | 配置npm镜像源,或使用pnpm |
| 组件样式不生效 | JSX里错用class | 改为className |
这张表看起来内容不多,但只要你不小心踩中其中一个,可能就得花上一整晚。初学者最怕的不是报错,而是报错的原因看不懂。先把这些高频坑避开,学习效率会提升很多。
5.4 一个容易被忽略的细节:不要在组件内部定义组件
还有个小坑,虽然不常见于最开始的练习中,但很多人在写了几个页面后就开始犯:在组件内部再定义一个组件,比如:
function Parent() { function Child() { return <p>子组件</p> } return <Child /> }每次Parent渲染时,Child都会被当作一个全新的组件类型,React会认为这是一个完全不同的组件,导致子组件的状态被重置、性能下降。正确做法是把Child定义在组件外部,和Parent平级。这个建议可能对纯新手来说有点超前,但提前知道,能省去很多性能优化的排查时间。
另外,组件命名一定要大写开头。React会根据首字母大小写判断这是否是一个组件。如果写成<child />,React会把它当成普通的HTML标签处理,结果就是页面渲染异常。这类问题控制台通常会提示“current component”,但有时候也会很隐晦,只有页面内容缺失一点一点对比才能发现。
我在实际带新人的过程中发现,大部分人学React最初的挫败感,并不是React本身有多难,而是他们跳过了“先跑起来、拆小段、再想原理”的步骤。你不需要一开始就精通Hooks的所有用法,也暂时不用研究虚拟DOM和diff算法的细节。只要能把项目创建出来,把组件拆开、传参、维护自身状态、循环渲染,然后把常见报错对应到解决方案上,React你说你已经入门了,一点也不为过。
最后分享一个我自己的习惯:每学一个新知识点,写一个不超过20行的小组件验证,比如一个计数器、一个待办输入框。跑通之后就把它放到项目里,替换成真实需要的数据。这样积累下来,你会发现自己手里攒了一批能用的积木。React归根结底就是搭积木的思维,先让每一块积木立得住,再把它们拼起来。保持这个节奏,入门就不会太痛苦。