1. 项目概述:为什么我建议你这样入门 React
在正式开始之前,先明确一个事实:React 不是一个框架,它是一个用于构建用户界面的 JavaScript 库。这句话听起来简单,但无数新人恰恰是因为没理解这一点,才会在上手阶段反复碰壁——你遇到的很多问题,其实是 JavaScript 本身的问题,而不是 React 的问题。零基础小白最容易犯的错,就是跳过 JS 基础直接冲进 React,结果连map、filter、解构赋值、展开运算符都还没摸透,就跑去写组件,最后被一堆报错折磨得怀疑人生。
这篇入门知识点覆盖的范围,是从安装环境开始,到 JSX、组件、Props、State、事件处理、表单、生命周期、Hooks、路由、状态管理,再到和 Vue 的差异对比、React Native 延伸方向,以及面试中真正会被问到的底层问题。我会用自己踩过的坑、在真实项目里积累的经验,把这条学习路径完整串起来。
适合谁看?零基础想系统学 React 的新人、被公司要求从 Vue 转 React 但一直没理的开发者、准备前端面试想快速梳理知识体系的人。看完这篇,不敢说你直接进大厂,但至少对着 React 项目不会两眼一抹黑,面试常规题也有底气答了。
2. 环境准备与工具选型:先把地基打牢
2.1 Node.js 版本怎么选,别在最开始就卡住
React 开发完全跑在 Node.js 生态上。你写的组件代码最终要靠构建工具打包,才能被浏览器识别,而构建工具本身就依赖 Node.js 环境。我的建议是:装 Node.js 最新的 LTS 版本,别追最新版,也别死守老版本。LTS 版本稳定、生态兼容性好,遇到问题搜解决方案时成功率最高。
安装完成后,打开终端输入node -v和npm -v,能看到版本号说明环境就绪了。这里有 Windows 用户经常踩的一个坑:安装包装完后,终端还是提示“node 不是内部或外部命令”,原因多半是环境变量没生效,重启终端甚至重启电脑就能解决,不用急着折腾 PATH。
国内开发者建议顺手把 npm 镜像切到淘宝源:
npm config set registry https://registry.npmmirror.com用的是 npx create-react-app 或 vite,国内源下载依赖的速度差距非常明显,实测能差出好几分钟。
2.2 用 Vite 还是 Create React App,我推荐你直接用 Vite
传统教程还在带新人用create-react-app(简称 CRA),但对 2024 年的新项目,我强烈建议直接用Vite。原因很直接:CRA 底层是 webpack,开发服务器启动要 20-60 秒,热更新也慢,你改一行代码可能要等好几秒才刷新;Vite 基于 esbuild,启动速度基本在 1-2 秒内,热更新几乎是毫秒级响应。对新手来说,越快的反馈意味着越高的学习效率。
创建项目只需要一行命令:
npm create vite@latest my-first-react-app -- --template react然后:
cd my-first-react-app npm install npm run dev浏览器打开终端提示的地址(一般是http://localhost:5173),就能看到你的第一个 React 页面。打开项目目录,你会看到src/App.jsx、src/main.jsx这几个关键文件,App.jsx是你之后主要折腾的地方,main.jsx负责把根组件挂载到 HTML 页面上。
2.3 编辑器配置:VS Code 必装插件清单
VS Code 是 React 开发事实上的标准编辑器。必装插件三个:ES7+ React/Redux/React-Native snippets(输入rfc可以秒生成函数组件模板)、ESLint(规范代码、报错提示)、Prettier(自动格式化)。装完后新建一个.prettierrc配置文件:
{ "semi": false, "singleQuote": true, "trailingComma": "all" }这三行配置的意思是结尾不加分号、字符串用单引号、多行结构最后加逗号,这是目前 React 项目最主流的代码风格。设置好后,保存文件时代码会自动整理,你只管写逻辑,格式交给工具处理。
3. JSX 语法:用写 HTML 的方式写 JavaScript
3.1 JSX 到底是什么,它和模板引擎的区别在哪
JSX 是 React 设计上最聪明也最容易让人误会的点。它看起来像 HTML,但本质上它就是一个 JavaScript 表达式。你写<div className="box">Hello</div>,经过 Babel 或 esbuild 编译后,会变成React.createElement("div", { className: "box" }, "Hello")这样一个函数调用。
很多从 Vue 转来的人习惯用v-if、v-for、插值表达式那套模板语法,在 JSX 里全都不是这样搞。JSX 里没有指令,你不是在“模板渲染”,你是在用函数组合的方式构建 UI 结构。理解这个转变至关重要,它会直接影响你写 React 的思路。
JSX 有三大纪律:
- 只能有一个根元素。多个兄弟元素必须包在一个
<div>或<>(Fragment 碎片)里。 - 所有标签必须闭合。
<img>要写成<img />,<input>要写成<input />。 class不能用,要用className。for不能用,要用htmlFor。原因很简单:class和for在 JavaScript 里是保留字,JSX 是 JS 的语法扩展,命名冲突避不开。
3.2 在 JSX 里写表达式:花括号的四边形法则
JSX 里想插入变量或表达式,必须用一对花括号{}包起来。比如写个动态标题:
const username = '张三' const isVIP = true function Greeting() { return ( <div> <h1>你好,{username}</h1> <span>{isVIP ? '尊贵的VIP用户' : '普通用户'}</span> </div> ) }注意几个规律:{}里可以是变量、算术运算、三元表达式、函数调用,但不可以放 if 语句、for 循环,因为它们是语句,不是表达式。想条件判断,用三元运算符或者&&短路运算;想循环渲染,用数组的map方法。
我在真实项目中经常看到新同事写类似{isVIP && <span>VIP标识</span>}这样的代码,这就是 React 社区最经典的&&短路条件渲染写法。当isVIP为true时,&&返回后面的 JSX 元素;为false时,返回false,React 会忽略不渲染它。
3.3 样式绑定与图片资源引入
JSX 里写行内样式,和 HTML 里完全不是一回事:
<div style={{ color: '#333', fontSize: '16px', backgroundColor: '#f5f5f5' }}> 样式对象 </div>外层大括号表示 JSX 表达式,内层大括号是对象字面量。注意属性名要改成驼峰命名法:background-color写成backgroundColor,font-size写成fontSize。
图片资源的引入也有讲究。public目录下的资源直接用绝对路径引用,src目录下的图片要import进来再使用,否则打包后路径会出错。推荐把组件相关资源放在组件目录下用import引入,这样 Webpack 或 Vite 会对它做哈希处理,缓存策略更好。
4. 组件体系:函数组件是主角,类组件是配角
4.1 函数组件与类组件:为什么新时代只推荐函数组件
React 组件有两个时代的产品:类组件和函数组件。类组件基于 ES6 的 class 语法:
import React, { Component } from 'react' class Counter extends Component { constructor(props) { super(props) this.state = { count: 0 } } increment = () => { this.setState({ count: this.state.count + 1 }) } render() { return ( <div> <p>当前计数:{this.state.count}</p> <button onClick={this.increment}>+1</button> </div> ) } } export default Counter函数组件则简洁得多:
import { useState } from 'react' function Counter() { const [count, setCount] = useState(0) return ( <div> <p>当前计数:{count}</p> <button onClick={() => setCount(count + 1)}>+1</button> </div> ) } export default Counter结论毫无悬念:新项目一律写函数组件 + Hooks。函数组件代码量更少、逻辑更集中、没有this指向问题、Hooks 机制比生命周期函数更易理解。类组件你只需要能看懂老项目的代码就够了,面试被问到了不至于懵。
4.2 Props 与 State:组件数据流的两个核心角色
Props 是父组件传给子组件的数据,子组件不能修改 props,它只读。State 是组件自己内部的数据,通过useState定义,通过setState函数修改。
当一个组件接收 props 时,函数组件的第一个参数就是 props 对象:
function UserCard({ name, age, avatar }) { return ( <div className="user-card"> <img src={avatar} alt={name} /> <p>{name},{age}岁</p> </div> ) } // 使用 <UserCard name="李四" age="25" avatar="/images/default.png" />props 的默认值和类型校验,用defaultProps和prop-types来定义:
import PropTypes from 'prop-types' function UserCard({ name, age }) { // ... } UserCard.propTypes = { name: PropTypes.string.isRequired, age: PropTypes.number } UserCard.defaultProps = { age: 18 }我给新人的建议是:项目里一定要写 prop-types。虽然 TypeScript 更严格更彻底,但零基础阶段引入 TS 会显著拉高学习曲线,先用 prop-types 养成类型意识,之后再平滑过渡到 TS。
4.3 组件拆分的原则:一个组件只干一件事
新手阶段最常见的病就是把所有 UI 塞进一个巨大的组件里,一个render里写了十几个区块的 JSX。这里我强烈建议你记住这条原则:当组件超过 200 行或 JSX 超过 50 行时,就应该拆分子组件。
一个拆分方案参考:
- 页面容器组件(负责数据请求和业务逻辑)
- 纯展示组件(只接收 props 渲染 UI,不关心数据从哪来)
- 公共基础组件(按钮、输入框、弹窗这类可复用的 UI)
拆分的实际收益在于:代码可读性提升、组件可复用、出问题时定位更快。在真实项目里,我见过 500 多行一个组件最后三天没人愿意改的情况——拆组件的功夫根本比不上维护的代价。
5. 事件处理与状态更新:React 运行机制的核心
5.1 事件绑定怎么用,参数怎么传
React 事件有一套自己的命名系统,都用驼峰写法并指向合成事件:onClick、onChange、onSubmit、onMouseEnter。事件绑定依旧是你熟悉的函数处理方式:
function Button() { const handleClick = (e) => { e.preventDefault() console.log('点击了按钮,事件对象是:', e) } return <button onClick={handleClick}>点我</button> }如果函数体只有一行,可以简写成箭头函数的方式。传参的话,直接套一层箭头函数是最通俗的做法:
<button onClick={() => handleDelete(user.id)}>删除</button>这里有个很多新人容易犯的错误:onClick={handleDelete(user.id)},在赋值时就先执行了函数,点击操作反而不会触发任何东西。正确做法永远是用箭头函数包一层。
5.2 setState 是异步的,别急着读最新值
setState是一个异步动作,调用后 state 不是立刻更新的。React 内部会对多个setState进行批处理,然后一次性聚合成一次渲染。经典案例:
const [count, setCount] = useState(0) const handleClick = () => { setCount(count + 1) setCount(count + 1) setCount(count + 1) console.log(count) // 还是 0,而且 count 最终只加了 1 }连续调用三次setCount(count + 1),count 最后只加了一次,因为每次拿到的都是旧值。如果想基于最新状态更新,必须用函数式更新:
setCount(prevCount => prevCount + 1) setCount(prevCount => prevCount + 1) setCount(prevCount => prevCount + 1)这样每次都在上一次更新结果的基础上继续叠加,最终结果就正确了。这是 React 面试高频题,也是实际开发中会遇到的坑。我在团队里见到过因为没搞懂这点,导致购物车数量重复相加只加一次的 bug。
5.3 状态更新后页面没变?大概率是引用没变
React 判断状态是否变化,用的是Object.is比较,简单理解就是比较引用地址。因此,修改对象和数组时,绝对不能直接修改原值,必须创建新的引用。
// 错误示范 const [user, setUser] = useState({ name: '张三', age: 20 }) user.age = 21 setUser(user) // 渲染不会更新,因为引用地址没变 // 正确写法 setUser({ ...user, age: 21 })数组也是同理:
// 错误示范 const [list, setList] = useState([1, 2, 3]) list.push(4) setList(list) // 不生效 // 正确写法:展开创建新数组 setList([...list, 4]) // 或者用 concat setList(list.concat(4))不可变数据是 React 社区最核心的开发哲学,所有状态更新都要以创建新副本为前提。这个理念刚开始会觉得麻烦,但习惯后会极大减少 bug 的产生,也为后面学习 Redux 打下了基础。
6. 条件渲染与列表渲染:业务开发中最高频的两种模式
6.1 四种条件渲染方式,哪一种该用于什么场景
React 没有像 Vue 那样的v-if,条件渲染靠 JavaScript 本身的能力完成。常见有四种写法:
- 三元运算符:
{isLogin ? <HomePage /> : <LoginPage />},适合两个分支的选择。 - 逻辑与
&&:{isVIP && <VipBadge />},适合只渲染一个分支、没有 else 需求时。 - 逻辑或
||:{data || <Loading />},数据没拿到时显示加载组件,适合做兜底。 - 立即调用函数:
{(() => { if (...) return ... })()},适合复杂多分条件逻辑,但代码可读性差,业务里一般会抽成函数组件。
在实际项目中,优先避免把太多条件嵌套在 JSX 里,可以提前定义变量:
function Dashboard({ user }) { let content if (!user) { content = <LoginPrompt /> } else if (user.isAdmin) { content = <AdminPanel /> } else { content = <UserPanel /> } return <div>{content}</div> }这样的代码线性可读,别人才看得懂,自己也看得懂。
6.2 map 渲染列表,key 是什么,为什么不能只用索引
列表渲染是前端业务的重头戏。React 里用map函数把数组转换成 JSX 数组:
const fruits = ['苹果', '香蕉', '橘子'] function FruitList() { return ( <ul> {fruits.map((fruit, index) => ( <li key={index}>{fruit}</li> ))} </ul> ) }这里 key 是 React 的 diff 算法比对新旧列表时的标识,直白说就是 React 靠 key 判断哪些列表项是新增、修改、删除的。用数组索引当 key 在大多数情况下是有隐患的。假设列表可以排序、删除、新增,索引会变化,React 可能把原本的 DOM 节点复用到错误的组件上,导致状态错乱。
我用一个经典例子解释:如果你有一个可删除的输入框列表,第一行输入了“hello”,删除第二行后,第一行的输入框会被复用给原来第二行的位置,但它的状态还是“hello”,界面就出 bug 了。
正确做法是使用唯一标识:
const items = [ { id: 'a1', text: '苹果' }, { id: 'b2', text: '香蕉' } ] {items.map(item => ( <Item key={item.id} text={item.text} /> ))}后端返回的数据如果有 id 字段,直接用;没有的话就考虑数据是否真的会变化。如果列表是纯静态的,用索引也无伤大雅,但最佳实践仍然是给每条数据维护一个唯一标识。
6.3 列表渲染中 key 和 diff 算法的关系,面试必问
React 的 diff 算法基于同级比较,逐层对比两个虚拟 DOM 树的差异,然后只更新变化的部分。List 里 key 的作用就是给 diff 一个“稳定的锚点”。
我给你一个面试时可以用的解释框架:React 在做列表 diff 时,会先对比 key 是否一致。key 相同就认为元素不变,直接复用;key 变了就卸载旧元素,创建新元素。如果没有唯一的 key,diff 就只能按位置对比,一旦数组顺序变化,React 会误判为所有元素都变了,要么多创建多余 DOM,要么把子组件内部状态复用到错误对象上。
7. 表单处理:受控组件与非受控组件
7.1 受控组件模式:React 管住输入框
在 React 中就两种表单模式,受控和非受控。受控组件的核心是:表单元素的值由 state 控制,用户在输入框里输入时,通过 onChange 事件更新 state,state 变化再反映回表单元素的 value。数据双向闭环,组件状态永远与页面显示保持一致。
import { useState } from 'react' function LoginForm() { const [form, setForm] = useState({ email: '', password: '' }) const handleChange = (e) => { const { name, value } = e.target setForm(prev => ({ ...prev, [name]: value })) } const handleSubmit = (e) => { e.preventDefault() console.log('提交的数据是:', form) // 这里写发送请求的逻辑 } return ( <form onSubmit={handleSubmit}> <input name="email" type="email" value={form.email} onChange={handleChange} placeholder="邮箱" /> <input name="password" type="password" value={form.password} onChange={handleChange} placeholder="密码" /> <button type="submit">登录</button> </form> ) }上面用name属性 + 计算属性名的方式,一套handleChange处理所有输入框。这是真实项目里最常用的表单处理方式,代码复用度高,不用每个输入框写一个独立的 onChange。
受控组件的好处是:数据实时掌握在 React 手里,方便做实时校验、联动逻辑(比如一个下拉选择影响另一个区域的内容显示)、拦截输入格式。代价是每次击键都会触发一次 setState 和重新渲染,性能上有轻微开销,但对于绝大多数业务场景,这种开销可以忽略。
7.2 非受控组件:用 ref 直接拿 DOM 值
非受控组件的思路是:表单元素自己维护内部状态,需要的时候用 ref 直接读取 DOM 的 value。
import { useRef } from 'react' function SimpleForm() { const inputRef = useRef(null) const handleSubmit = (e) => { e.preventDefault() console.log('输入的值是:', inputRef.current.value) } return ( <form onSubmit={handleSubmit}> <input ref={inputRef} type="text" placeholder="说点什么" /> <button type="submit">提交</button> </form> ) }什么时候用非受控组件?文件上传控件input type="file"的值是只读的,没法走受控模式;一些第三方库(比如富文本编辑器)内置了自己的状态管理,你强行控制 value 反而会出问题。除此之外,我绝大多数场景都建议用受控模式,那样数据链路清晰、调试方便。
7.3 表单验证的两个实操细节
一是不要只依赖 onChange 做必填校验,那会导致用户啥都没做就开始标红,体验很差;正确的交互节奏是:首次提交时把所有错误一次性显示出来,之后每次输入变更都重新校验,输入正确后错误提示自动消失。
二是提交后要重置表单。受控组件下重置很简单:
setForm({ email: '', password: '' })二是不用单独调用 DOM 的 reset 方法,因为 value 完全由 state 控制,state 清空了,输入框里的内容自然就清空了。
8. 生命周期与 Hooks:从“何时触发”到“想触发就触发”
8.1 函数组件没有生命周期,但有 useEffect
类组件的生命周期有componentDidMount、componentDidUpdate、componentWillUnmount三个阶段。函数组件里,这些全部由useEffect一个 Hook 接管。你不再需要记忆生命周期方法的名称,只需要理解:“当某个依赖变化时,我想做什么”。
useEffect 的四个使用场景:
// 1. 组件挂载后执行一次(模拟 componentDidMount) useEffect(() => { fetchData() }, []) // 2. 依赖变化后执行(模拟 componentDidUpdate 的部分场景) useEffect(() => { document.title = `你有 ${count} 条新消息` }, [count]) // 3. 组件卸载时清理(模拟 componentWillUnmount) useEffect(() => { const timer = setInterval(() => { console.log('定时任务执行中') }, 1000) return () => { clearInterval(timer) console.log('组件卸载,清理定时器') } }, []) // 4. 每次渲染都执行(不传依赖数组) useEffect(() => { console.log('组件每次渲染后都会执行') })第二和第三个场景是实际开发中用途最频繁的:发送网络请求、订阅事件、操作浏览器 API、启动定时器、设置监听器、清理定时器和取消订阅。
8.2 useEffect 的依赖数组:最重要的一个配置项
依赖数组是 useEffect 的第二个参数,它决定了 effect 在什么时候执行。三种写法对应三种行为:
- 不传依赖数组:每次渲染后都执行。
- 传空数组
[]:只在组件第一次挂载后执行一次。 - 传入变量数组
[count, user]:任一依赖变化后执行。
翻阅真实项目代码时,最多的 bug 往往出在依赖数组上。要么漏了依赖导致闭包捕获旧值,要么依赖过多导致函数频繁触发。这里我给你三条实操经验:
第一,网络请求类的 effect 依赖数组选空数组,意思就是只在挂载时请求一次。
第二,useEffect 里如果用了外部变量,尽量把它加进依赖数组,否则前端工具的开发模式下也会警告提示你闭包过期值问题。
第三,获取最新值时不要试图绕过依赖数组,用useRef保存最新值,或者把逻辑拆分到多个 effect 里。
8.3 必学的 Hooks 全家桶:useState、useEffect、useRef、useMemo、useCallback
函数组件的动力系统就是 Hooks,掌握下面这几个就足够应付至少九成业务需求。
useState:给函数组件添加局部状态,前面已经用过多次,不赘述。
useRef:保存一个可变值,改变它不会触发重新渲染,同时可以访问 DOM 节点。
const timerRef = useRef(null) const startTimer = () => { timerRef.current = setInterval(() => { console.log('运行中') }, 1000) } const stopTimer = () => { clearInterval(timerRef.current) }useMemo:缓存计算结果。当依赖不变时,多次渲染直接从缓存读取,而不是重新计算。适合复杂数据处理场景。
const totalPrice = useMemo(() => { return products.reduce((sum, p) => sum + p.price * p.count, 0) }, [products])useCallback:缓存函数本身。当函数被传给子组件时,父组件重新渲染不会导致子组件重复接收新的函数引用,避免不必要的子组件重渲染。
const handleSave = useCallback(() => { saveData(form) }, [form])我给新人的建议是:不要一开始就滥用性能优化 Hooks。先写清晰的普通函数,出现性能问题时再针对性地用 useMemo 和 useCallback,否则只会把代码变得复杂难读。
8.4 自定义 Hook:把逻辑抽出来复用
Hook 可以封装到独立函数里供多个组件复用,这就是自定义 Hook。命名必须以use开头,这样 React 才能识别它是一个 Hook,允许在内部调用其他 Hook。
举个例子,封装一个窗口宽度监听的 Hook:
import { useState, useEffect } from 'react' function useWindowWidth() { const [width, setWidth] = useState(window.innerWidth) useEffect(() => { const handleResize = () => setWidth(window.innerWidth) window.addEventListener('resize', handleResize) return () => window.removeEventListener('resize', handleResize) }, []) return width } // 组件中使用 function Layout() { const width = useWindowWidth() const isMobile = width < 768 return ( <div> {isMobile ? <MobileMenu /> : <DesktopSidebar />} </div> ) }自定义 Hook 让逻辑复用变得异常优雅,组件间的公共逻辑(请求数据、表单状态、鉴权、主题切换)全都可以封装成独立 Hook。这也是面试中区分初级和中级工程师的一个考察点。
9. 组件通信与状态管理:当数据不再局限于一个组件内
9.1 单向数据流与状态提升
React 的数据流是自顶向下的单向数据流。父组件通过 props 把数据传给子组件,子组件通过回调函数把事件和数据传给父组件。这条链路看起来简单,但新手经常会滥用。
当多个兄弟组件需要共享同一个状态时,最简单的方案是状态提升:把公共状态放到最近的祖先组件中,由祖先通过 props 把这个状态分发给各个子组件,子组件修改数据的请求也通过回调传递回祖先组件。
function Parent() { const [activeTab, setActiveTab] = useState('home') return ( <div> <TabNav activeTab={activeTab} onTabChange={setActiveTab} /> <TabContent activeTab={activeTab} /> </div> ) }这种模式的优点是数据来源单一、可预测性强、调试方便;缺点是组件层级深时,“prop drilling”问题会出现——中间层组件不需要这个数据,但被迫接过 props 再传给更下层。
9.2 跨层级通信:Context 一把梭
跨越多层组件传递 props,层级一深就变得很折磨。React 的 Context API 能直接解决这个问题,它可以在组件树中“注入”数据,任何层级的子组件都能直接读取,不需要逐层透传。
import { createContext, useContext, useState } from 'react' const ThemeContext = createContext('light') function App() { const [theme, setTheme] = useState('light') return ( <ThemeContext.Provider value={{ theme, setTheme }}> <Header /> <Content /> <Footer /> </ThemeContext.Provider> ) } function Header() { const { theme, setTheme } = useContext(ThemeContext) return ( <div className={`header ${theme}`}> <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> 切换主题 </button> </div> ) }Context 适合全局性、低频变化的数据:主题、用户登录信息、语言偏好、权限配置。但它也有缺点——Context 变化时,所有消费这个 context 的组件都会重新渲染,如果数据结构很大、更新太频繁,会有性能隐患。所以大的全局状态管理,我更推荐下面要说的状态管理库。
9.3 状态管理库:Redux 还在用吗?要不要学 zustand
React 状态管理的演进速度很快。老牌王者Redux依然是大厂项目的标配,但 Redux Toolkit 的引入已经大大简化了原来的模板代码。红遍社区的Zustand则以极简的 API 和天然的 hook 集成为特征,受到了新兴项目的偏爱。
我结合实际使用感受给你做个对比:
| 对比维度 | Redux Toolkit | Zustand |
|---|---|---|
| 学习曲线 | 较陡,需要理解 store、reducer、action、dispatch | 平缓,接近 React state 的使用方式 |
| 模板代码量 | 较多,需要约定的文件结构 | 极少,几乎零模板 |
| 调试工具 | Redux DevTools 非常成熟 | 有,但生态相对逊色 |
| 适合场景 | 大型应用、复杂业务逻辑、团队协作 | 中小型项目、追求快速开发 |
Zustand 的用法简单到看一眼就会:
import { create } from 'zustand' const useCounterStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })) })) function Counter() { const { count, increment, decrement } = useCounterStore() return ( <div> <button onClick={decrement}>-</button> <span>{count}</span> <button onClick={increment}>+</button> </div> ) }我给新人的学习建议:先熟练管理组件局部 state,再掌握 Context,最后按需引入状态管理库。不要一上来就套 Redux,绝大多数场景根本用不到。
10. 路由、网络请求与工程化:React 应用的完整拼图
10.1 React Router 的路由体系,与 Vue Router 差异在哪
单页应用(SPA)无法离开路由而存在。React 生态主流的路由库就是React Router(当前最新为 v6)。基本用法:
import { BrowserRouter, Routes, Route, Link, useNavigate } from 'react-router-dom' function App() { return ( <BrowserRouter> <nav> <Link to="/">首页</Link> <Link to="/about">关于</Link> <Link to="/users/123">用户详情</Link> </nav> <Routes> <Route path="/" element={<HomePage />} /> <Route path="/about" element={<AboutPage />} /> <Route path="/users/:id" element={<UserDetail />} /> </Routes> </BrowserRouter> ) } // 在组件中跳转 function HomePage() { const navigate = useNavigate() const goToAbout = () => navigate('/about') return <button onClick={goToAbout}>去关于页</button> }对比 Vue Router,React Router v6 的核心差异有三点:第一,Vue Router 的路由配置是配置文件加内置的<router-view>,React Router 则是组件化路由,路由就是 JSX 的一部分;第二,Vue Router 的导航守卫是内置功能,React Router 里要通过useEffect或封装高阶组件实现类似的用户鉴权逻辑;第三,动态路由参数的获取方式不同,Vue 里是route.params.id,React 里是useParams()函数解构出来。这几点在面试中被问的概率相当高。
在业务场景下,如果项目本身就是 React 技术栈,我建议直接使用 React Router,没必要为了“类比熟悉”强行切换框架。
10.2 网络请求:fetch 还是 axios
React 本身不含 HTTP 请求库,需要自己接,常用选择有两个。浏览器的原生fetchAPI 够用但封装程度低:
const loadData = async () => { try { const response = await fetch('/api/users') if (!response.ok) { throw new Error(`请求失败:${response.status}`) } const data = await response.json() setList(data) } catch (err) { console.error('请求出错:', err) } }axios则是社区最主流的解决方案,它具有浏览器兼容性好、支持请求拦截器、响应拦截器、自动 JSON 转换、取消请求等特性。配合请求/响应拦截器,可以在一个地方统一处理 token 携带、错误提示、统一 loading 控制。
import axios from 'axios' const api = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动加 token api.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器:统一处理错误 api.interceptors.response.use( response => response.data, error => { if (error.response?.status === 401) { // 跳转登录页 } return Promise.reject(error) } )我给新人的建议:先学会用 fetch 理解原理,项目开发时直接用 axios,效率更高。
10.3 从零到一:一个真实 React 项目的完整搭建流程
把前面所有内容串起来,新手完成一个简单管理后台的流程是:
- 使用 Vite 初始化项目,安装依赖。
- 安装
react-router-dom、axios、zustand(如果业务复杂度需要)。 - 按路由划分页面组件,建立
pages、components、hooks、utils、services等目录结构。 - 在
services目录封装 API 请求函数,统一管理接口。 - 开发公共组件(按钮、表格、弹窗、表单),再开发页面组件。
- 在页面组件中使用自定义 Hook 封装数据获取、loading、错误处理逻辑。
- 用路由配置串联页面,配合懒加载
React.lazy(() => import(...))做代码分割。 - 开发阶段用 mock 数据,联调时切换 API 地址。
一条实用经验:在前端项目里,目录结构即是团队的规范。新人接手项目时,先看目录结构就能猜出这个团队的水平和代码组织风格。我习惯的最小目录结构是:
src/ ├── api/ # 接口请求 ├── components/ # 全局公共组件 ├── hooks/ # 公共自定义 Hook ├── pages/ # 页面级组件 ├── store/ # 全局状态 ├── utils/ # 公共工具方法 ├── App.jsx # 路由配置入口 └── main.jsx # 应用启动入口11. React 与 Vue 的选型对比:别再纠结了
新手问得最多的问题就是“React 和 Vue 到底学哪个”,许多人在选择阶段就卡住了。我从技术思想和业务场景两个维度给一个务实的答案。
技术上,Vue 的核心理念是“渐进式”和“模板语法”,模板更贴近传统 HTML,适合新人快速上手做出界面;React 的核心理念是“一切皆 JavaScript”,UI 只是数据映射出来的函数结果。React 写法的 JavaScript 特性更强,但要求开发者本身对 JS 基础有更扎实的掌握。从状态管理、组件拆分到路由实现,React 给出的“自由度”更高,这意味着你有了更多选择权,也意味着你要自己做出更多决策。
业务上,如果是中小型项目、展示型页面、后台可视化,Vue 开发效率确实更高;如果是大型团队、长期维护的复杂应用、对组件抽象和逻辑复用要求高,React 的生态优势和工程化成熟度更明显。
我个人的建议很简单:你接下来要去投递的公司主要用什么技术栈,就主攻什么。都还是学生或者没目标,那就选 React。原因是 React 的生态体系更大、就业市场更广、周边工具的学习路径也自然延伸到了 React Native 等方向,一门语言的触角更宽。
12. React Native:同一套思想,延伸到移动端
React 的知识并不仅限于 Web 端。React Native允许你用 JavaScript 和 React 的组件模型,开发 iOS 和 Android 原生应用。它的价值在于:Web 前端工程师可以复用 React 的知识和技能,直接进入移动端开发领域。
React Native 的核心是桥接机制:JavaScript 层运行着 React 逻辑,通过桥层与原生 UI 组件通信。你在 JS 里写<View>、<Text>,最终会映射成原生平台上的 UIView 或 Android View,因此 App 的外观和交互感受接近原生应用,而不是一个网页套壳。
对零基础小白的建议:先问 Web 端 React 是否理解透彻了,再考虑学习 React Native。否则两组概念交叉在一起——Web 的 DOM 模型和 React Native 的原生组件模型完全不是一回事,容易崩溃。常用的启动白屏问题也与此有关:原生层面首先要加载 JavaScriptBundle,这段时间 UI 是空白的,需要做启动屏优化。
13. 面试高频题与新手常见坑,一次补齐
React 面试几乎绕不开以下核心问题,我按类别整理出来并附上答题思路。
13.1 面试题速查表
| 问题 | 答题要点 |
|---|---|
| 什么是虚拟 DOM | 用 JavaScript 对象描述真实 DOM 结构,操作时先改虚拟 DOM,通过 diff 更新最小变更部分,减少不必要的真实 DOM 操作 |
| key 的作用 | 给 diff 提供对比锚点,让 React 知道列表项的身份。用稳定唯一 id,避免用随机数和索引 |
| 函数组件和类组件区别 | 代码形式不同,类组件有 this 和生命周期,函数组件用 Hooks 实现同等能力、代码更简洁 |
| setState 是异步还是同步 | React 18 自动批处理下多为异步,函数式更新可基于最新状态 |
| useEffect 与生命周期映射 | 空依赖对应 DidMount,有依赖对应 DidUpdate,返回函数对应 WillUnmount |
| 组件间通信方式 | 父子用 props 和回调,跨多层级用 Context,复杂全局状态用状态管理库 |
| React 路由鉴权实现 | 封装守卫组件,没登录时用<Navigate to="/login" />重定向 |
| 如何性能优化 | React.memo 包裹组件、useMemo 缓存计算、useCallback 缓存函数、列表虚拟化、代码分割懒加载 |
13.2 新手最容易踩的坑
坑一:在 map 渲染的循环体内写业务逻辑。比如在 JSX 里直接写if/else、复杂的计算式,代码可读性极差。正确做法是基于数据先计算好渲染内容,再在 JSX 内做展示即可。
坑二:滥用index当 key 导致样式错乱。前文已详述,列表一旦涉及顺序变动,务必用唯一 id。
坑三:组件卸载后还在 setState。常见于异步请求已经发出但组件被销毁,回调里仍然执行了 setState,控制台直接报错“Can't perform a React state update on an unmounted component”。处理方案:用useRef记录组件是否已卸载,或者在 useEffect 的清理函数里设置标志位。
function useSafeSetState() { const isMounted = useRef(true) useEffect(() => { return () => { isMounted.current = false } }, []) const safeSetState = (setter, value) => { if (isMounted.current) { setter(value) } } return safeSetState }坑四:直接修改 props 或 state 导致无渲染更新。引用不变问题前面已讲,检查一下你是不是直接改了数组或者对象原值。
坑五:在 render 中定义内联对象导致子组件 mome 失效。用 React.memo 优化的子组件,如果父组件每次渲染都创建新的对象 props,memo 完全失去效果,因为引用每一次都不同。
// 错误示范 <Child config={{ type: 'primary', size: 'large' }} /> // 正确做法:用 useMemo 保持引用稳定 const config = useMemo(() => ({ type: 'primary', size: 'large' }), []) <Child config={config} />14. 我的学习路径建议与最后几句掏心话
如果你是完全零基础,我私心建议的学习路径应该是这样的:先花两周把 JavaScript 的 ES6 语法过一遍,重点掌握let/const、箭头函数、解构赋值、模板字符串、展开运算符、数组的map/filter/reduce、Promise和async/await;然后开始学 React 基础,从 JSX 语法、组件、State、Props 一点点写过来,每学一个知识点就写一个小 demo 练手;接着掌握 Hooks、React Router、网络请求,尝试做一个完整的 To-Do 应用或简易博客;最后再接触状态管理库和工程化工具,了解 Redux 或 Zustand 解决了什么问题。
不要一上来就扑到源码里。我看过太多新人把 React 源码当作第一学习素材,啃了三天什么也没记住,深受打击后放弃。源码是进阶的范畴,而入门更重要的从来都是“能跑起来、能写出来、能 debug 出来”。
写 React 这两年半,最大的成长感悟就是:React 告诉你 UI = f(state),它会逼着你用 JavaScript 的思维去思考界面。你写的每一行 JSX 都只是一个函数的返回值,你定义的数据才是整个应用的灵魂。数据设计好了、状态拆分合理了,组件自然而然就好了。
最后分享一个实战技巧:遇到报错不要怕,记住三个步骤——先读报错信息里的组件栈定位问题文件,再看代码里最近的几行有没有引用错误或空值判断问题,最后用控制台的console.log打印关键变量的值确认数据是否符合预期。
React 是一条越到后面越呈现魅力的技术路线。开头会有点磕磕绊绊,但把这篇基础版的每个知识点都动手实跑一遍,再回去看你公司的老项目,一定能看懂七八成了。到那时候,你就正式从“零基础小白”毕业了。