news 2026/10/23 13:42:23

零基础学React完整路线:从JS基础到组件化实战,避开新手常见坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
零基础学React完整路线:从JS基础到组件化实战,避开新手常见坑

1. 零基础学React,先想清楚这几件事

1.1 React到底是什么,值不值得花时间学

学React之前,我建议你先搞清楚一个基本事实:React不是一个全家桶框架,它本质上是一个用来构建用户界面的JavaScript库。核心思想就两条:组件化和声明式UI。组件化意味着你把页面拆成一个个独立的小块,每个小块自己管自己的状态和渲染;声明式意味着你只需要描述"界面应该长什么样",不用操心"每一步怎么操作DOM"。这两点恰恰是零基础同学最容易绕晕的地方——因为你之前写原生JS时,习惯是"先拿元素、再改属性、再监听事件",到了React这儿这套思维要整个翻过来。

零基础学React难吗?说实话,难的不是React本身,而是你是否有足够的JavaScript功底。ES6的箭头函数、解构赋值、展开运算符、数组的map/filter,这些写不好,React的代码你会看得很吃力。但如果你已经能熟练写原生JS,React的入门门槛其实不高——花一两周把核心概念过一遍,再动手做两三个小项目,基本就能上手干活儿。如果你完全是编程小白连JS都不熟,那我建议先把JS基础补一补再回来,否则很容易卡在"代码跟着抄能跑,换个需求就不会改"的尴尬阶段。

这套内容适合谁?两类人:一是刚学完HTML/CSS/JS、想找第一份前端工作或实习的准开发者;二是用Vue或jQuery写了好几年业务、想拓宽技术面的在职前端。前者需要系统的概念梳理和实战路径,后者需要快速对比理解React的思维方式。我下面讲的路线、原理、踩坑经验,都是按这两类人的真实需求来的。

1.2 一份可执行的学习路线:别上来就刷视频

零基础同学最容易犯的错,是囤了一堆视频教程,跟着敲了俩月,合上电脑啥也不会。学习前端框架,唯一的捷径是"小步快跑、边学边写"。我给你的建议路线是这样:

第一阶段(1周):把ES6+核心语法过一遍。重点盯住const/let、箭头函数、模板字符串、解构赋值、展开运算符、Promise和async/await、数组的map/filter/find。不用学得多深,见到能认识、能写就行。

第二阶段(2周):熟悉JSX和组件基础。JSX就是在JavaScript里写类似HTML的语法,它会被编译成React.createElement调用。你要搞清楚三件事:怎么用函数定义组件、怎么给组件传props、怎么用useState管理内部状态。

第三阶段(1周):学Hooks和副作用。useState和useEffect是前期最重要的两个Hooks。useState管数据,useEffect管"数据变化后要做什么"——发请求、操作DOM、设置定时器,全在这里。

第四阶段(1-2周):做实战项目。我后面会完整拆一个待办事项应用,把CRUD、筛选、搜索、本地存储全部走一遍。做完这一个项目,你基本就能应对初级岗位的日常开发了。

第五阶段(进阶,持续学习):路由、状态管理、TypeScript、性能优化、工程化配置。这些不用一开始全学,遇到需求再补最高效。

提示:不要用"看完某某教程"作为学习进度的衡量标准,用"我能独立完成某某功能"来替代。每个阶段结束前,给自己出一个小挑战,比如第二阶段结束试着写一个"计数器"或"图片轮播",第四阶段结束试着把待办事项加上"编辑"功能。

1.3 动手之前的环境准备和工具选型

工欲善其事,必先利其器。零基础阶段,环境别折腾得太复杂,但也别用太老的工具链。

Node.js:去官网下载LTS(长期支持)版本,用node -v验证安装。React生态的工具链全都跑在Node上,没有它寸步难行。

包管理器:新手我推荐直接用npm,它是Node自带的,不需要额外学习成本。等你项目多了、感觉到"安装依赖太慢"了,再换pnpm或yarn。

脚手架:现在新项目建议直接用Vite,别再碰Create React App(CRA)了。CRA已经停止维护,而Vite启动速度快、配置简单、社区生态也成熟。创建项目的命令就一行:

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

创建完进目录,安装依赖,启动开发服务器:

cd my-app npm install npm run dev

编辑器:VS Code加上ES7+ React/Redux扩展插件就够了。这个插件提供JSX代码片段,输入rfce回车就能生成一个函数组件模板,能省不少打字时间。

有一点提醒你:刚开始别花太多精力去折腾"完美配置"——比如什么ESLint规则、Prettier自定义、路径别名、构建优化,这些都等你写了几个月业务代码再搞也不迟。零基础阶段最重要的是跑起来、看到东西、理解数据怎么流动。

2. 核心概念拆解:组件、JSX、状态与props

2.1 组件思维:从函数组件开始

我见过太多零基础的同学栽在"组件"这词上,觉得它很玄。其实组件就是一个返回JSX的函数,仅此而已。你完全可以把它理解成一个自定义的HTML标签,这个标签封装了自己的结构、样式和行为。

function Card({ title, content }) { return ( <div className="card"> <h2>{title}</h2> <p>{content}</p> </div> ); }

上面的Card就是一个组件。它接收外部传入的title和content,渲染成一张卡片。你可以在任何地方复用它:

function App() { return ( <div> <Card title="第一张卡片" content="这是内容一" /> <Card title="第二张卡片" content="这是内容二" /> </div> ); }

为什么要拆组件?两个原因:复用和可维护性。页面上的每个区域如果都是独立的组件,改一块不会影响另一块,多人协作时也不会疯狂冲突。打个比方:组件化前的页面就像一张巨大的画布,谁都在上面画,改一笔可能蹭掉别人的地方;组件化之后就像乐高积木,每个积木独立成型,拼接方式清清楚楚。

写React组件建议直接用函数组件,别再学类组件了——虽然网络上老教程还在用class App extends Component,但React官方早已明确函数组件+Hooks是主流方向。类组件的this绑定问题、生命周期方法的理解成本,对新手的劝退效果实在太强。

2.2 JSX语法要点:看着像HTML,实际是JavaScript

JSX是React最让人"又爱又恨"的部分。爱它是因为写起来直观,恨它是因为有些规则和HTML不一样,新手很容易踩坑。

规则一:class要写成className。因为class是JavaScript的关键字,在JSX里用它会有冲突,所以改用className。类似的还有for要写成htmlFor。

规则二:JSX里写JS表达式要用大括号。<div>{count}</div>是渲染变量,<div>{count + 1}</div>是渲染计算结果,<div>{isShow ? <p>显示</p> : null}</div>是条件渲染。

规则三:列表渲染必须加key。用数组.map()生成一组元素时,React需要靠key来识别哪些元素被新增、删除、修改。key的类型最好是字符串或数字,而且要稳定——不要用数组下标当key,否则当列表发生插入、删除、排序时,React会"认错人"导致渲染错乱。比如一个待办列表,中间删掉一条时,下标key会让后续所有元素的复用关系全部错位,可能出现勾选状态串行的问题。

// 推荐 { todos.map(todo => ( <TodoItem key={todo.id} todo={todo} /> ))} // 不推荐:用数组下标当key { todos.map((todo, index) => ( <TodoItem key={index} todo={todo} /> ))}

规则四:JSX注释要写在一对花括号里,{/* 这是注释 */}。直接在JSX里写// 注释会直接报错。

规则五:JSX必须有一个根节点。你可以用<div>包一层,也可以用<>碎片(Fragment)包一层。碎片不会产生多余的DOM节点,适合不想破坏布局结构的场景。

这些规则记不住没关系,报错信息都会指向问题位置,写几次自然就熟练了。真正要理解的是:JSX本质上是React.createElement的语法糖,你写的每一个标签都会被编译成对象描述,然后被React渲染成真实DOM。理解了这一层,你就会明白为什么JSX里能写JS逻辑、为什么要讲"声明式UI"。

2.3 useState与useEffect:React数据流动的起点

对零基础同学来说,useState和useEffect是打通React任督二脉的两个Hook。

useState用来在组件里存储状态。这里的"状态"说白了就是"会变化的数据"。用户输入、接口返回、页面开关状态,都属于状态。

function Counter() { const [count, setCount] = useState(0); return ( <div> <p>当前计数:{count}</p> <button onClick={() => setCount(count + 1)}>+1</button> </div> ); }

注意setCount是异步的,而且调用它会触发组件重新渲染。state更新后,React会重新执行这个组件函数,拿到最新的JSX,再对比上一次的虚拟DOM,计算出最小更新量,更新真实DOM。这就是React性能好的底层逻辑——它尽量少碰真实DOM,而是先在虚拟层"算好账"。

useEffect管的是"副作用",也就是那些不属于纯渲染逻辑的操作:发HTTP请求、操作localStorage、设置定时器、手动修改DOM。

function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { // 模拟发起请求 fetch(`/api/user/${userId}`) .then(res => res.json()) .then(data => setUser(data)); }, [userId]); // 依赖数组:userId变时才重新执行 return <div>{user ? user.name : '加载中...'}</div>; }

依赖数组[userId]是useEffect的第二个参数,它决定了这个副作用"什么时候重新执行"——只有依赖项变化时才会执行。如果省略依赖数组,每次渲染都会执行,配合异步请求会导致无限请求循环,这是新手最容易踩的大坑,我后面在第五部分还会展开讲。

还有一个常见的理解偏差:useEffect不能代替事件处理。用户点击按钮时改数据,应该直接在onClick里写逻辑,而不是靠useEffect监听某些值变化再改其他值,那样会把数据流变得很难追踪。

3. 从基础到进阶:自定义Hooks、路由与状态管理

3.1 自定义Hooks:把重复逻辑抽出来的利器

写了两个月业务代码后,你一定会遇到一个情况:好几个组件都在做同样的事——比如"从接口取数据、处理加载中、处理错误"。如果每个组件里都复制一遍useEffect+fetch逻辑,代码会很臃肿,而且改接口地址时得满项目找。

这时候就该上自定义Hooks了。React的Hooks机制允许你把逻辑提取成以use开头的函数,内部可以自由使用useState和useEffect。它不像组件需要返回JSX,它只返回你需要的数据和操作。

我以最常用的"数据请求"为例,写一个useFetch:

import { useState, useEffect } from 'react'; function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { // 防止组件卸载后setState let cancelled = false; setLoading(true); setError(null); fetch(url) .then(res => { if (!res.ok) throw new Error('请求失败'); return res.json(); }) .then(data => { if (!cancelled) setData(data); }) .catch(err => { if (!cancelled) setError(err); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [url]); return { data, loading, error }; }

使用时,任何组件都只关心三件事:

function ArticleList() { const { data: articles, loading, error } = useFetch('/api/articles'); if (loading) return <p>加载中...</p>; if (error) return <p>出错了:{error.message}</p>; return <ul>{articles.map(a => <li key={a.id}>{a.title}</li>)}</ul>; }

这个自定义Hook里我加了一个cancelled标记——组件如果已经卸载,数据才返回时再去setState,React会警告"对卸载组件执行状态更新"。用这个标记可以很优雅地规避。类似的场景还有防抖搜索、窗口尺寸监听、表单校验,都能做成自定义Hooks。判断标准就一条:如果两份组件代码里各有一段几乎一样的逻辑,它就是自定义Hooks的候选者。

3.2 React Router v6:页面跳转与路由传参

零基础阶段做的单页面小项目不涉及路由,但只要开始做真正的Web应用,一定会有"点击导航切换页面"的需求。React生态里最主流的路由方案是React Router,目前稳定版本是v6。

最基本的用法,用createBrowserRouter或者传统的BrowserRouter+Routes都可以。我倾向推荐v6的写法,结构更清晰:

import { BrowserRouter, Routes, Route } from 'react-router-dom'; import Home from './pages/Home'; import Article from './pages/Article'; import NotFound from './pages/NotFound'; function App() { return ( <BrowserRouter> <Routes> <Route path="/" element={<Home />} /> <Route path="/article/:id" element={<Article />} /> <Route path="*" element={<NotFound />} /> </Routes> </BrowserRouter> ); }

路由传参有两种形式,一种是路径参数/article/:id,组件里通过useParams读取:

import { useParams, useNavigate } from 'react-router-dom'; function Article() { const { id } = useParams(); const navigate = useNavigate(); return ( <div> <h1>文章:{id}</h1> <button onClick={() => navigate(-1)}>返回上一页</button> </div> ); }

另一种是通过useSearchParams读取查询字符串(URL里的?xx=yy):

function SearchPage() { const [searchParams] = useSearchParams(); const keyword = searchParams.get('keyword'); // ... }

写路由时有个常见问题:开发环境一切正常,部署到服务器后刷新子页面变成404。这是因为静态服务器收到/article/123这个路径时找不到对应文件,兜底返回了404页面。解决办法是在服务器端配置"所有未知路径都回到index.html",让React Router接管路由。如果你用的是Nginx,加一个try_files $uri $uri/ /index.html;基本就解决了。

3.3 状态管理:从Context到Redux到Zustand,各有什么适用场景

组件之间的数据传递,零基础阶段靠props逐层传,项目变大后你会发现一层层传props很痛苦——A组件的数据要传给C组件,中间B组件根本不用它,还得带着。这时候需要状态管理方案。

React自带的Context适合"主题切换、用户登录信息、语言偏好"这类全局低频数据。把数据放到Provider里,消费者组件通过useContext读取,代码很简洁。缺点是一旦Context里的值频繁变化,所有消费它的组件全部重新渲染,性能不好控制。

Redux Toolkit是传统大型项目的主流选择。它有DevTools(时间旅行调试)、有统一的action/reducer结构,适合多人协作且状态交互极复杂的大型应用。但学习曲线陡,概念多——dispatch、action、reducer、selector、slice,新手容易迷失在概念里。零基础同学不用上来就学Redux,先写几个项目,感到props传值真的很痛苦了再学不迟。

Zustand是我现在个人比较中意的轻量方案。它没有Provider包裹,创建store的API很简洁,写法几乎是直觉级的:

npm install zustand
import { create } from 'zustand'; const useCartStore = create((set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), removeItem: (id) => set((state) => ({ items: state.items.filter(i => i.id !== id) })), })); // 任意组件里直接使用 function CartBadge() { const items = useCartStore((state) => state.items); return <span>{items.length}</span>; }

Zustand的极简写法让状态管理不再可怕,而且性能好、TypeScript支持好。我的建议是:中小型项目,Zustand够用就别上Redux;大型项目若团队已经有Redux习惯,继续用Redux Toolkit也完全合理。状态管理工具最重要的是"解决问题、不添乱",凡是为炫技而引入的方案,最后都会变成项目的维护负担。

4. 实战项目:从零搭建一个React待办应用

4.1 项目需求设计:先想清楚要做什么,再动手写代码

理论学再多,不落地都是空中楼阁。这一节我用一个经典的待办事项应用,带你走一遍完整的React实战流程。需求先定清楚:

  • 能添加待办事项,内容不能为空
  • 能标记完成/未完成,能删除事项
  • 能按全部/未完成/已完成/搜索关键词筛选
  • 数据存localStorage,刷新页面不丢失
  • 底部显示剩余未完成数量

功能不多,但CRUD、筛选、持久化、状态管理全覆盖了,作为第一个练手项目非常合适。

组件结构设计:我把界面拆成5个组件。

App ├── TodoInput (输入框+添加按钮) ├── TodoFilter (筛选按钮组+搜索框) ├── TodoList (事项列表) │ └── TodoItem(单条事项) └── TodoFooter (剩余数量统计)

数据流是这样的:App持有todos这个状态和所有操作函数(添加、切换、删除),通过props传给各个子组件。状态提升是React里很重要的设计思路:多个子组件要共享同一份状态时,就把状态提到它们最近的共同父组件里。

这样设计的好处是,数据流向永远是单向的:父组件往下传数据和行为函数,子组件调用函数通知父组件更新数据。你追踪数据变化时,永远不需要"猜测"某个值是在哪里被改的。

4.2 核心代码实现:用一个自定义Hook管理待办状态

首先初始化项目和基础目录:

npm create vite@latest react-todo -- --template react cd react-todo npm install npm run dev

然后我把整份待办状态和localStorage持久化逻辑提取成一个自定义Hook,App组件只负责"使用"它:

import { useState, useEffect } from 'react'; const STORAGE_KEY = 'react-todo-items'; function useTodos() { // 惰性初始值:只从localStorage读一次 const [todos, setTodos] = useState(() => { const saved = localStorage.getItem(STORAGE_KEY); return saved ? JSON.parse(saved) : []; }); // 每次todos变化都同步到localStorage useEffect(() => { localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); }, [todos]); const addTodo = (text) => { if (!text.trim()) return; const newTodo = { id: Date.now(), // 简单用时间戳做id,够用 text: text.trim(), completed: false, }; setTodos([...todos, newTodo]); }; const toggleTodo = (id) => { setTodos(todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo )); }; const deleteTodo = (id) => { setTodos(todos.filter(todo => todo.id !== id)); }; return { todos, addTodo, toggleTodo, deleteTodo }; } export default useTodos;

简单说一下几个关键点:

第一,useState传的是个函数而不是直接传值,这叫"惰性初始化"——React只会用这个函数的返回值作为初始state,而函数本身只在首次渲染执行。对于读取localStorage这样有开销的操作,用惰性初始化可以避免每次渲染都重新读一次。

第二,addTodo里的id用Date.now(),单机场景够用。如果以后做多人协作要考虑用更严格的唯一id方案,比如crypto.randomUUID()。

第三,你可能会问:setTodos([...todos, newTodo])为什么不直接todos.push(newTodo)?因为React的不可变性原则——永远不要直接修改state,要创建一个新数组替换它。React靠"引用变了没"来判断值是否更新,直接push引用没变,React不会重新渲染。

4.3 组件联调与效果验证:把功能串起来

App组件把Hook提供的数据和函数分发给各个子组件:

import useTodos from './hooks/useTodos'; import TodoInput from './components/TodoInput'; import TodoFilter from './components/TodoFilter'; import TodoList from './components/TodoList'; import TodoFooter from './components/TodoFooter'; import { useState } from 'react'; function App() { const { todos, addTodo, toggleTodo, deleteTodo } = useTodos(); const [filter, setFilter] = useState('all'); // all | active | completed const [keyword, setKeyword] = useState(''); // 过滤逻辑在App里完成,子组件只负责展示 const visibleTodos = todos.filter((todo) => { const matchFilter = filter === 'all' ? true : filter === 'active' ? !todo.completed : todo.completed; const matchKeyword = todo.text.toLowerCase().includes(keyword.toLowerCase()); return matchFilter && matchKeyword; }); const activeCount = todos.filter(t => !t.completed).length; return ( <div className="todo-app"> <h1>我的待办</h1> <TodoInput onAdd={addTodo} /> <TodoFilter filter={filter} onChange={setFilter} keyword={keyword} onKeywordChange={setKeyword} /> <TodoList todos={visibleTodos} onToggle={toggleTodo} onDelete={deleteTodo} /> <TodoFooter activeCount={activeCount} /> </div> ); } export default App;

子组件里,TodoList用map渲染TodoItem:

function TodoList({ todos, onToggle, onDelete }) { if (todos.length === 0) { return <p className="empty">暂无待办事项</p>; } return ( <ul className="todo-list"> {todos.map(todo => ( <TodoItem key={todo.id} todo={todo} onToggle={onToggle} onDelete={onDelete} /> ))} </ul> ); }

TodoItem负责展示和事件触发:

function TodoItem({ todo, onToggle, onDelete }) { return ( <li className={todo.completed ? 'completed' : ''}> <input type="checkbox" checked={todo.completed} onChange={() => onToggle(todo.id)} /> <span>{todo.text}</span> <button onClick={() => onDelete(todo.id)}>删除</button> </li> ); }

这一步很关键:子组件里不用自己维护状态,操作全交给父组件传下来的函数。React里管这叫"受控组件"模式——子组件的展示内容完全由props决定,行为完全由父组件传入的回调函数驱动。这样做的好处是,以后改需求,比如加一个"批量删除"功能,你只需要在App层增加函数,子组件根本不用动。

完成后跑一下npm run dev,浏览器打开本机地址。测试清单如下:

  • 添加"学习React"事项,列表立刻出现
  • 勾选事项,文字加上删除线,底部剩余数减少
  • 切换到"已完成"筛选,只剩已勾选项
  • 删除事项,对应的筛选结果同步更新
  • 刷新页面,所有数据还在(localStorage生效)

全部通过,恭喜你,第一个React应用完整落地了。如果中途报错,先看浏览器控制台的红字提示,React的报错信息比大多数框架都要友好,通常会直接告诉你是哪个组件、哪个变量出了问题。

5. 常见问题与踩坑实录:React新手翻车现场

5.1 useEffect无限循环与依赖陷阱

问题现象:写完useEffect后,浏览器像是卡死了,终端疯狂打印请求日志。

原因:useEffect里调用了setState,而这个state又在依赖数组里,或者依赖数组没传,导致每次渲染完副作用更新state,state更新又触发渲染,如此死循环。

// 反例:死循环 function BadExample() { const [count, setCount] = useState(0); useEffect(() => { setCount(count + 1); // 每次渲染后都setState,又触发渲染 }, [count]); // count变了,useEffect再跑 }

正确姿势:如果只想在挂载时执行一次,依赖数组传[];如果需要根据响应数据更新state,直接在异步回调里setState,不要把"请求结果"这种会变的依赖写在数组里。如果确实需要在effect里基于旧值计算新值,用函数式更新setCount(prev => prev + 1),然后把依赖数组清空。

5.2 闭包陷阱:为什么setState之后立刻读到的还是旧值

问题现象:点击按钮后,日志打印出来的count还是旧值。

function Counter() { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); console.log(count); // 还是0,不是1 }; return <button onClick={handleClick}>点击</button>; }

原因:handleClick闭包捕获的是本次渲染时的count值。setCount(count + 1)只是告诉React"下次渲染请用count+1",但本次渲染的handleClick里count仍然是0。要读取最新值,应该在useEffect里去读:

useEffect(() => { console.log('count已更新为:', count); }, [count]);

或者,如果你是基于最新state做计算,优先用函数式更新:

setCount(prev => prev + 1);

这样永远不会拿旧值计算新值。我在带新人时见过无数人在这里纠结,记住一句话:React渲染是分版本的,你在"这一版"里永远读不到"下一版"的值。

5.3 key重复导致的渲染错乱

问题现象:列表某项勾选后,另一项的样式跟着变,或者删除中间项时删错对象。

原因:用了数组下标当key。列表重新渲染时,React通过key识别元素,下标key会遇到"复用了错误的组件实例"这个尴尬。

最简单可靠的方案:数据是新增时才生成唯一id(时间戳、UUID都行)。如果是从接口拿的数据,用接口返回的id字段。如果确实没有任何天然id字段,才考虑用"索引+自定义前缀"这类妥协方案。

5.4 受控组件输入卡顿:中文输入法触发onChange过早

问题现象:用受控input输入中文时,拼音字母也实时进了state,导致输入框行为很奇怪。

这个问题其实不只是React的锅,而是浏览器composition事件(输入法组词过程)与onChange的时序问题。React在受控组件模式下,每次onChange都强制state覆盖输入框值,可能打断输入法组词。

解决方案:如果业务场景对中文输入有要求(比如搜索框里的联想词),判断是否处于"组词中"状态:

function SearchBox({ value, onChange }) { const [isComposing, setIsComposing] = useState(false); return ( <input value={value} onChange={(e) => { if (!isComposing) onChange(e.target.value); }} onCompositionStart={() => setIsComposing(true)} onCompositionEnd={(e) => { setIsComposing(false); onChange(e.target.value); // 组词结束后再补一次 }} /> ); }

5.5 为什么子组件的useEffect不执行了

问题现象:把父组件的数组或对象作为props传到子组件,子组件useEffect监听这个props,但父组件更新了数据后子组件的effect没触发。

原因:React对依赖项的比较是引用比较(Object.is),不是内容比较。如果父组件把同一个数组引用传下去,子组件依赖的"地址"没变,effect自然不触发。需要用useMemo缓存"当数据真正变化时才生成的新引用":

// 父组件 const visibleTodos = useMemo(() => { return todos.filter(...); }, [todos, filter, keyword]);

这样只有todos、filter、keyword真正变化时,visibleTodos才是一个新数组,子组件的useEffect才能感知到变化。

5.6 性能优化常见误区:防抖不如节流?memo是不是越多越好?

很多同学的项目React运行顺畅,于是开始"预防性"性能优化——把每个组件都用memo包一层、到处用useMemo和useCallback。我见过不少反面教材:优化加了半天,运行反而变慢了,因为memo的浅比较本身也有开销,useMemo还要额外维护依赖分析。

我的建议很简单:

  • 页面卡了,先用React DevTools的Profile面板找出真正频繁重渲染的组件,针对它优化
  • useMemo、useCallback、memo是优化工具,不是默认配置。用它们之前先回答一个问题:"这个计算真的昂贵吗?这个子组件真的因为多余渲染而卡顿了吗?"
  • 最立竿见影的优化往往是结构层面的:把大组件拆成小组件、把频繁变化的状态放到合适的层级、列表项渲染尽量保持key稳定

当然也有值得默认加的场景——列表组件里的子项、被memo化的复杂图表组件、以及你的组件会被同一个父组件多次复用且渲染成本较高的情况。别被"性能优化圣地"的极端观点带节奏,反应到用户可感知的体验上才算有效优化。

写在最后:React学习路上的三个心得体会

带过不少零基础入行的同事,自己也踩过无数坑,说几个观点供你参考。

第一,写代码不要追求一次写对,要习惯"跑起来调试"。很多人学习时特别怕报错,一红就慌。其实报错是React在帮你,它甚至会把错误定位到具体文件和行号。把心态从"代码必须一次通过"换成"报错是我和编译器之间的正常对话",你的学习速度会快很多。

第二,每一行代码都要能回答"为什么"。我见过太多人抄代码时把useState初始值从0改成1,但问他为什么用Hook,他说"教程这么写的"。这种状态学三个月会陷入"能做但不会改"的瓶颈。一个很好的自检办法:写完一个功能,合上代码,用自己的话解释一遍"数据是怎么从输入变成输出的"。解释不了的地方,就是你的知识缺口。

第三,别在工具链和"最佳实践"上过度焦虑。网上关于React的争议帖子很多,"该不该用Redux""函数组件是不是一定比类组件好""要不要直接上Next.js",这些对初学者来说都是噪声。React生态最重要的能力是"用最朴素的方式解决问题",等你遇到真实的瓶颈,自然知道该引入什么工具。到那时候,你已经有足够的判断力了。

这个项目做完之后,你可以试着自己扩展:加一个"编辑待办"功能,把TodoItem改成可双击编辑;或者把筛选条件加到URL查询参数里,实现"刷新后筛选状态不丢失";再进一步,利用useReducer重构状态管理,去感受一下和useState的差异。React的进阶就是一个接一个小实战堆出来的,保持这个节奏,半年后回头看,你会惊讶于自己走过的距离。

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

Agent+Harness工作流:LLM信息处理与上下文超长优化实战

1. 从“躺平挖 alpha”说起&#xff1a;这套工作流到底在解决什么问题“躺平挖 alpha”这个说法第一次看到的时候我笑了很久&#xff0c;但仔细一想&#xff0c;它精准地描述了一种状态&#xff1a;你不想每天手动去翻几百条信息、盯十几个数据源、重复做那些机械的筛选和整理动…

作者头像 李华
网站建设 2026/10/22 1:54:20

AnyPS5:Linux下PS5 GPU硬件能力重映射工具链解析

1. 项目概述&#xff1a;AnyPS5不是PS5模拟器&#xff0c;而是一套面向Linux开发者的PS5硬件能力复用工具链AnyPS5这个标题在当前技术社区里容易引发第一层误解——很多人会下意识联想到“PS5模拟器”或者“在PC上跑PS5游戏”。但实际完全不是这么回事。我从去年底开始接触这个…

作者头像 李华
网站建设 2026/10/22 3:44:15

锐捷RGNOS接口配置实战:VLAN划分与链路聚合避坑指南

简介&#xff1a;《锐捷网络RGNOS-接口配置指南》是一份面向网络工程师、运维人员及网络初学者的接口配置手册&#xff0c;聚焦锐捷RGNOS系统下接口相关知识点&#xff0c;帮助读者理清物理接口、逻辑接口等类型差异&#xff0c;掌握从基础到高级的配置方法。资源为单个PDF文件…

作者头像 李华
网站建设 2026/10/22 3:44:16

信息学奥赛买笔问题:贪心策略与分支结构详解

2059&#xff1a;【例3.11】买笔&#xff0c;这道题我相信学过信息学奥赛的同学都不陌生。它是《信息学奥赛一本通》第三章选择结构部分的经典例题&#xff0c;题目讲的是买笔这件事——钢笔5元一支、铅笔2元一支、圆珠笔1元一支&#xff0c;要求三种笔都至少买一支&#xff0c…

作者头像 李华
网站建设 2026/10/22 14:11:57

Windows 10驱动签名强制机制详解与安全绕过实践

简介&#xff1a;本资源是一份面向Windows 10系统管理员、驱动开发者及IT运维人员的实操指南&#xff0c;聚焦解决因驱动程序未签名导致无法安装的典型问题&#xff0c;提供禁用强制签名的完整、可复现的操作路径。文档以图文结合方式详述从设置入口到F7键启用“禁用驱动程序签…

作者头像 李华
网站建设 2026/10/22 18:24:34

基于JSP的网上书店系统设计与实现:从环境搭建到购物车事务全解析

简介&#xff1a;一份基于JSP的网上书店系统毕业设计论文文档&#xff0c;主要面向计算机相关专业学生&#xff0c;尤其适合正在准备课程设计或毕业设计的读者。该文档完整阐述了网上书店系统的设计与实现过程&#xff0c;内容涵盖JSP与JAVA技术基础、系统可行性分析与需求分析…

作者头像 李华