ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

React入门核心教程:组件、props、state与useEffect实战解析

React入门核心教程:组件、props、state与useEffect实战解析 1. 先说清楚React 到底是干嘛的我第一次接触 React 的时候最困惑的事情不是“它怎么用”而是“它到底解决了什么问题”。当时还在用 jQuery 写页面手动操作 DOM改一个数据要写好几行document.getElementById还经常因为忘记更新某个节点导致页面状态和实际数据不一致。React 给出的答案是你只需要关注数据长什么样页面长什么样React 帮你算。它把整个界面拆成一个个组件每个组件接收数据、返回界面描述当数据变化时React 自动对比前后差异只更新需要变化的部分。这种“数据驱动视图”的思路就是 React 的核心也是后来 Vue、Svelte 等框架的共同方向。这套教程适合谁如果你是前端新手只学过 HTML CSS JavaScript 基础想学框架又怕一上来被各种术语劝退那这篇精华版能帮你直接把 React 最核心的骨架搭起来。如果你有后端经验想转前端这篇也能让你快速理解组件化开发的思维方式。我尽量把每个概念都用大白话讲配上完整的运行示例核心代码直接在本地跑起来看到效果比看十篇概念文章都管用。2. 组件页面就是乐高积木2.1 函数组件的本质就是“会返回 JSX 的函数”React 里最常用的组件形式就是函数组件。说白了它就是一个返回 JSX 的 JavaScript 函数。JSX 是 React 专门设计的语法长得像 HTML但实际上是 JavaScript 的语法糖。浏览器不认识 JSX所以需要编译工具把它转换成React.createElement调用。function Greeting() { return h1你好React/h1; }这段代码里Greeting就是一个组件。它不接收任何输入返回一段 JSX。要在页面上使用它只需要在别的地方写成Greeting /就行。组件的名字必须是大写字母开头如果是小写React 会当成普通的 HTML 标签处理自定义组件就失效了。这个细节我见过很多新手踩坑包括我自己。组件的好处在于复用。比如一个博客首页文章卡片、侧边栏、导航栏每个区域都可以独立成组件。改卡片样式只动卡片组件不会影响其他地方。这种模块化能力是组件化开发最大的收益也是 React 能撑起大项目的基础。2.2 JSX 不是模板语言大括号里写 JavaScriptJSX 跟传统的模板语言比如 Vue 的{{ }}插值最大的区别是它直接嵌在 JavaScript 里写起来非常自由。任何 JavaScript 表达式都可以放进{}里const user { name: 小明, age: 18 }; function UserCard() { return ( div p姓名{user.name}/p p年龄{user.age 1}/p p{user.age 18 ? 成年 : 未成年}/p /div ); }注意{}里只能放表达式不能放语句比如if。如果你想写条件逻辑用三元表达式代替或者先在外面算好变量再放进 JSX。另外还有一个细节JSX 里给元素加 class不能写class要写className。因为 JSX 最终会被转成 JS 对象class在 JavaScript 里是保留字所以 React 用className替代。同理for属性要写成htmlFor。刚写 React 经常会犯这个错尤其以前写过 HTML 的人特别容易顺手就写成class。如果返回的 JSX 有多行组件外面必须套一个根标签或者用.../碎片Fragment包裹。多行 JSX 必须有一个共同的父级容器否则会报错。3. 跑一个能看的项目环境准备其实很简单3.1 用 Vite 而不是 Create React App现在官方推荐的脚手架已经偏向 Vite 了老牌的 Create React App 虽然还在维护但构建速度明显不如 Vite而且配置起来比较重。Vite 是基于原生 ES Module 的开发时启动快得惊人几秒钟就能把页面跑起来。先确认电脑上装了 Node.js建议 16 以上的版本。打开终端执行npm create vitelatest my-react-app -- --template react cd my-react-app npm install npm run devnpm create vitelatest会拉取 Vite 的创建工具-- --template react指定用 React 模板。装完依赖后npm run dev会启动一个本地开发服务器终端会打印出访问地址一般是http://localhost:5173浏览器打开就能看到默认页面。如果不想自己敲命令也可以直接在 Vite 官网的初始化页面里选择 React 模板下载 zip 解压后跟着 README 跑起来。本质上是一样的只是少了命令行操作。3.2 看一眼脚手架目录结构Vite 的 React 模板目录不长核心要关注几个文件和文件夹my-react-app/ ├── node_modules/ // 依赖包 ├── public/ // 静态资源 ├── src/ │ ├── App.jsx // 根组件 │ ├── main.jsx // 入口文件React 从这里启动 │ ├── index.css // 全局样式 │ └── ... ├── index.html // 唯一的 HTML 页面 └── package.json // 依赖和脚本main.jsx是入口内容通常长这样import React from react; import ReactDOM from react-dom/client; import App from ./App.jsx; import ./index.css; ReactDOM.createRoot(document.getElementById(root)).render( React.StrictMode App / /React.StrictMode );ReactDOM.createRoot(...).render(...)是 React 18 的标准写法。createRoot创建一个根节点render方法把App /挂载到页面上。StrictMode是严格模式开发阶段会帮我们检测组件里写得不对的地方比如副作用放在渲染函数里它会在开发环境重复执行渲染函数来暴露问题这条我是真实验证过的生产环境它不会生效所以不用担心影响性能。4. props组件之间传数据就像传参数4.1 props 都是单向的父组件传给子组件组件之间共享数据靠props。父组件在子组件标签上写属性子组件通过函数参数接收。// 父组件 function App() { return ( UserCard name小红 age{20} / ); } // 子组件 function UserCard({ name, age }) { return ( div h3{name}/h3 p{age} 岁/p /div ); }{ name, age }是解构赋值的写法直接从 props 对象里取出对应字段。如果没有解构也可以写成props.name和props.age但解构让代码更简洁。一个重要的原则是props 是只读的。子组件不能修改传进来的 props一旦你试图去给它赋值React 会直接在控制台给你报错。如果子组件想修改数据正确的做法是发送通知给父组件让父组件去改这也对应了 React 的“单向数据流”设计。我刚学的时候总觉得这个限制很麻烦为什么不让我在子组件里直接改后来才明白单向数据流让数据来源变得清晰。页面上任何数据变化你都能顺着组件层级往上追溯排查问题快得多。如果数据可以随便改最后很可能变成一团乱麻改一处崩一处。4.2 动态传值在标签属性里用大括号props 不光能传字符串字面量任何 JavaScript 表达式都能传。function App() { const user { name: 阿强, age: 22 }; const showAge true; return ( Card name{user.name} age{user.age} hobbies{[篮球, 吉他]} showAge{showAge} onClick{() alert(点击了卡片)} / ); }hobbies直接传了一个数组onClick传了一个函数。React 里这种自由传值方式让组件可以非常灵活地组合。有人可能觉得“组件里传函数”很别扭但这恰恰是 React 数据交互的核心父组件把自己定义的处理函数传给子组件子组件在合适的时机调用它等于是子组件在“请求”父组件做事情。给组件写 props 时我习惯把跟事件相关的都以on开头命名比如onClick、onChange、onSubmit。这个命名习惯不是硬性要求但多人协作时能快速分清哪些是数据、哪些是回调。5. state让组件记住自己的状态5.1 useState组件里的“记忆盒子”props 是父组件塞给子组件的那子组件自己内部的数据变化怎么办用useState。useState是 React 自带的一个 Hook它返回两个东西当前的状态值和一个更新这个状态的函数。import { useState } from react; function Counter() { const [count, setCount] useState(0); return ( div p当前计数{count}/p button onClick{() setCount(count 1)}加一/button /div ); }useState(0)给count定的初始值是 0。setCount(newValue)会更新状态并且自动触发组件重新渲染。也就是说你只要更新状态页面上对应的部分就会自动跟着变化不需要手动操作 DOM。这是 React 最核心的体验把 UI 同步的脏活累活交给框架。这里要特别强调不能用count count 1这样直接赋值的方式去修改状态。组件里直接修改普通变量不会有任何响应式效果必须要调用setCount。React 只有通过状态更新函数去改数据才会感知到变化并重新渲染。5.2 状态更新的异步特性与函数式更新React 的状态更新是异步的。比如const [count, setCount] useState(0); function handleClick() { setCount(count 1); console.log(count); // 这里打印出来的还是旧值 0 }这段代码里console.log(count)打印出来的依然是0因为setCount只是安排了这次更新并没有立刻改变count的值。如果连续点击按钮三次希望 count 变成 3只调三次setCount(count 1)很可能得不到预期结果因为每次拿到的count可能都是旧的 0。正确的做法是用函数式更新setCount(prevCount prevCount 1);prevCount是 React 传入的最新状态值基于这个值做递增才是准确的。这个写法在多个更新连在一起、或者自己在循环里更新状态时尤其重要。我第一次写计数器就踩了这个坑连点三次按钮count 只变成了 2。后来查了文档才知道异步更新的细节。5.3 状态该放在哪个组件这是新手最容易拿不准的问题。常见的经验法则是哪个组件用到这个状态或者状态影响哪些组件的显示就往它们的共同父组件放。举个例子用户输入框和提交按钮都在同一个表单里那输入内容这个 state 就放在表单这个父组件里。如果两个兄弟组件需要展示同一个数据那就把 state 放到它们共同的父组件通过 props 分别传给它们。如果项目大了状态共享变复杂再考虑引入全局状态管理工具比如 Redux、Zustand。但初学阶段千万别急着上状态管理库先把 useState 用熟练再用自定义 Hook 去抽你重复的状态逻辑这往往比引入外部库更简单直接。6. 事件处理让页面有反应6.1 在 JSX 里监听事件React 监听事件的方式跟原生 DOM 很像但有几处不同。原生 HTML 里写onclickfunc()React 里写onClick{handleClick}。注意两点一是事件名用驼峰写法onClick而不是onclick二是传的函数引用不是字符串。function ClickButton() { function handleClick() { alert(你点了按钮); } return button onClick{handleClick}点我/button; }如果需要往事件处理器里传参数有几种写法function ClickButton() { function handleClick(msg) { alert(msg); } return button onClick{() handleClick(来自按钮的消息)}点我/button; }用箭头函数包一层是最常见的做法简洁明了。注意事项是不要直接写onClick{handleClick(消息)}那会在渲染阶段就直接执行这个函数而不是等到点击的时候。6.2 事件对象与合成事件React 的事件处理器会接收一个合成事件对象SyntheticEvent它包装了浏览器的原生事件兼容了不同浏览器的差异。常规操作原生的e.preventDefault()或e.stopPropagation()在这里同样可用。function SubmitForm() { function handleSubmit(e) { e.preventDefault(); // 阻止表单默认刷新 const formData new FormData(e.target); console.log(formData.get(username)); } return ( form onSubmit{handleSubmit} input nameusername / button typesubmit提交/button /form ); }e.preventDefault()是表单场景里最常见的需求——不刷新页面而是在前端处理后端数据。理解了合成事件后面做表单的受控组件会顺手很多。7. 条件渲染与列表渲染7.1 条件渲染的三种姿势React 没有 Vue 里那种v-if指令一切都是写 JS 逻辑三元表达式最简单直观的写法。短路逻辑如果条件为真就渲染后面部分否则什么都不渲染。立即执行函数适合条件复杂的情况但不推荐在 JSX 里写复杂的 IIFE逻辑复杂时应提前抽离。function LoginStatus({ isLogin }) { return ( div {isLogin ? p欢迎回来/p : button登录/button} /div ); } function Notice({ count }) { return ( div {count 0 p你有 {count} 条未读消息/p} /div ); }第一种写法适合简单的二中选一第二种适合“条件成立才显示”的场景。两种都很常见。用短路逻辑时注意别让count是 0 这样容易引起整型显示为 0 的坑。如果count是0表达式会渲染出数字 0。比如{count 0 p.../p}在 count 是 1 时返回p在 count 是 0 时直接返回falseReact 会自动忽略false不渲染。但如果你写{count p.../p}count 为 0 时 React 会当真渲染出数字 0界面出现一个孤零零的 0这类细节需要自己体感踩过才知道有多尴尬。7.2 列表渲染key 的作用与选取渲染一个数组最常见的就是用.map()const fruitList [苹果, 香蕉, 橘子]; function FruitList() { return ( ul {fruitList.map((fruit, index) ( li key{index}{fruit}/li ))} /ul ); }.map()返回的数组里每个元素必须有一个key属性。key是 React 用来识别“这个元素是哪一个”的线索。如果列表可能增删、排序那key就不能用数组下标否则 React 在更新时可能把顺序搞错导致状态错乱。最简单可靠的key是数据自带的唯一 ID比如用户 ID、商品 ID 等。key只需要在兄弟节点间唯一就行不需要全局唯一。还有一点key不会作为 prop 传进组件你在组件内部props.key是拿不到的。列表渲染里还有个常见需求过滤后的列表。不要直接改原始数组而是用.filter()生成新数组渲染。React 的理念是“不修改原数据每次生成新数据”这跟函数式编程的思想一脉相承。8. useEffect处理副作用的地方8.1 useEffect 到底管什么组件里的“副作用”指的是跟纯渲染无关的外部操作。比如发请求、操作 localStorage、订阅事件、修改页面标题等。React 推荐把副作用放在useEffect里执行。useEffect接收两个参数一个函数和一个依赖数组。import { useEffect, useState } from react; function UserPage({ userId }) { const [user, setUser] useState(null); useEffect(() { fetch(/api/user/${userId}) .then(res res.json()) .then(data setUser(data)); }, [userId]); if (!user) return p加载中.../p; return h1{user.name}/h1; }这段代码表示当userId变化时useEffect 里的函数会重新执行去请求新的用户数据。如果依赖数组是空数组[]则只在组件挂载时执行一次。如果完全不传第二个参数那就每次渲染都执行——这个行为往往会导致无限循环需要谨慎。8.2 清理副作用防止内存泄漏如果你的副作用是订阅事件、开启定时器或建立 WebSocket 连接那么组件卸载时要清理这些资源否则会浪费内存甚至产生 bug。useEffect的函数可以返回一个“清理函数”React 会在组件卸载时调用它useEffect(() { const timer setInterval(() { console.log(定时器触发); }, 1000); return () { clearInterval(timer); }; }, []);这里返回的() { clearInterval(timer); }就是清理函数。React 在清理旧副作用、卸载组件时都会调用它。我做过一个长列表滚动加载的页面忘了清理scroll事件监听页面退出了监听还在导致滚动时疯狂触发请求排查了大半天才发现是清理函数没写。经验法则凡是setInterval、addEventListener、new WebSocket这类全局性的东西都要在 useEffect 里配对清理。8.3 依赖数组的取舍别在依赖里写变了但没用到的变量依赖数组的作用是告诉 React“只有这些值变了才需要重新执行这个副作用。”但我见过最多的坑是——开发调试时为了“省事”把依赖数组省略不写结果组件每次渲染都触发副作用卡成幻灯片。另一个极端是每次渲染都调用useEffect但函数内部的变量其实在依赖数组中没写全React 只会对比依赖数组里的变量是否变化如果里面没写但实际用到的外部变量变了副作用执行的时机就不对。此时 React 编译器会提示你补充依赖或者你需要重新审视副作用的设计。我个人的习惯是不要让 useEffect 里用太多外层变量尽量把需要的数据集中在依赖数组里明确列出。如果逻辑复杂到依赖太多就先在 effect 外部把变量整理成一个对象或数组再依赖它这样逻辑边界清楚后续维护也更轻松。9. 新手最容易踩的 8 个坑9.1 在 JSX 里写循环或 if 出错JSX 里写for循环、if语句会直接报错。因为{}里只接受表达式。有循环先在外面算好结果有 if 用三元表达式代替。// 错误写法 return div{if (a) { b }}/div; // 正确写法 return div{a ? b : null}/div;9.2 setState 之后马上读 state 读不到新值这是异步更新导致的。如果需要基于最新 state 做后续逻辑要么放在useEffect里根据 state 变化去处理要么用函数式更新计算。9.3 忘记 key 或使用 index 当 key列表渲染时控制台会警告 “Each child in a list should have a unique key”。如果列表有增加、删除或重排用 index 会有隐藏的 bug。尽量用唯一 id。9.4 改了状态页面没变直接给 state 对象做属性修改const [obj, setObj] useState({ count: 0 }); // 错误 obj.count 1; setObj(obj); // 正确 setObj({ ...obj, count: obj.count 1 });React 判断状态更新靠的是引用变化。直接修改对象属性引用没变React 认为没更新不会重新渲染。必须生成一个新对象来替换旧对象。9.5 组件内部大量逻辑堆在 return 里JSX 里塞太多三元表达式和函数调用会让可读性崩掉。把关键计算移到 return 前面分成变量或函数保持 JSX 干净。9.6 useEffect 造成无限循环最常见的写法是useEffect(() { setCount(count 1); });没传依赖数组副作用在渲染后执行执行后更新 state触发重渲染副作用又执行死循环。解决办法就是给依赖数组明确的依赖项。9.7 组件命名忘记大写自定义组件用小写开头时React 会把它当成 HTML 标签像userCard /会被当成一个不认识的原生标签最终渲染出一个空的自定义元素页面上什么都看不到。这个问题新手经常碰见控制台还会警告。9.8 事件处理函数里使用了箭头函数但没绑 this这在类组件里非常常见但现在大家基本写函数组件不存在这个问题。不过我还见到有人会不小心在一个函数组件里给另一个函数组件传递方法时忘记用箭头函数包装导致方法在渲染期间直接被调用。原则就是传的是函数引用不是函数调用的结果。10. 一个完整的入门示例待办事项把上面讲到的知识点组合起来写一个经典的小项目——待办事项列表。麻雀虽小五脏俱全props、state、事件、条件渲染、列表渲染、useEffect 全都有了。先创建组件结构但这个示例可以全部写在App.jsx里import { useState, useEffect } from react; function TodoApp() { const [todos, setTodos] useState(() { const saved localStorage.getItem(todos); return saved ? JSON.parse(saved) : []; }); const [inputValue, setInputValue] useState(); useEffect(() { localStorage.setItem(todos, JSON.stringify(todos)); }, [todos]); function addTodo() { if (!inputValue.trim()) return; const newTodo { id: Date.now(), text: inputValue, done: false }; setTodos([...todos, newTodo]); setInputValue(); } function toggleTodo(id) { setTodos(todos.map(todo todo.id id ? { ...todo, done: !todo.done } : todo )); } function deleteTodo(id) { setTodos(todos.filter(todo todo.id ! id)); } return ( div h1待办事项/h1 input value{inputValue} onChange{e setInputValue(e.target.value)} placeholder输入新任务 / button onClick{addTodo}添加/button ul {todos.map(todo ( li key{todo.id} input typecheckbox checked{todo.done} onChange{() toggleTodo(todo.id)} / span style{{ textDecoration: todo.done ? line-through : none }} {todo.text} /span button onClick{() deleteTodo(todo.id)}删除/button /li ))} /ul {todos.length 0 p还没有任务先添加一个吧。/p} /div ); } export default TodoApp;在这个例子里todos初始值是从 localStorage 读取的注意useState里传的是函数() {...}这样初始化时只会执行一次。useEffect监听todos变化自动写入 localStorage刷新页面数据不丢。添加、切换、删除都用不可变更新方式生成新数组不修改原数组。列表渲染中用了todo.id作为 key。条件渲染处理了空列表的提示。这段代码可以直接替换脚手架里的App.jsx跑起来试试。本地打开页面添加几条任务刷新浏览器数据还在这会让你真切感受到 state 和 useEffect 的威力。11. 我对 React 学习路径的实际体会写了几年 React我把入门阶段的经验总结成一句话先用起来再理解原理最后才是优化。初学者最容易犯的错误是想先把原理全部搞懂再动手写代码。React 的 useState 异步更新、虚拟 DOM 对比、fiber 架构这些原理确实重要但如果你还没写过 50 个组件的项目上来啃源码只会挫败感很强。先照着例子写几个小项目等遇到状态不同步、性能卡顿的问题再去翻原理那时你会更容易理解它为什么这么设计。第二个建议是不要太早引入 Redux。很多人刚学完 useState 就听说“状态多了要用 Redux”结果 Redux 那一套 action、reducer、dispatch 让新手直接晕倒。实际上很多场景用useState加 props 传递就能解决。等你真的遇到跨层级组件共享状态的痛点再学 Zustand 或者 Redux Toolkit会轻松得多。我见过太多新手卡在 Redux 的学习曲线里最后放弃了整个 React。第三个建议是写组件的时候把“这个组件负责什么”想清楚。一个组件只做一件事props 不要传太多超过五六个就该停下来想想是不是该拆组件了。这样写出来的代码自己能看懂别人也能维护。如果你自己动手把上面的待办事项跑通再改改样式、加个编辑功能React 入门的门槛就已经跨过去了。后面的路无非是遇到问题、查文档、解决问题循环往复水平自然就起来了。
返回列表