从DOM脚本到基于状态的UI:React的核心模型
为何 React 用声明式组件取代手动 DOM 更新,以及 JSX、props、state、重新渲染和单向数据流是如何构成一个统一的思维模型的。
通过简单的DOM操作,就能轻松创建一个可以更新计数器、更换图标并播放动画的点赞按钮。但如果在实时动态内容中加入50个这样的按钮,且每个按钮还要同时记录评论、分享次数以及保存状态,手工编写的DOM代码就会变得难以应对。React的存在正是为了解决这类问题:你只需描述当前数据下界面应有的样子,React会自动判断需要进行哪些DOM更改。
本指南将按逻辑顺序逐步讲解实现这一目标的原理:JSX、组件、属性、状态、重新渲染、声明式风格,以及数据和事件在组件树中的传递方式。学习完之后,你应该能够预判组件何时会重新渲染,判断某个状态信息应存储在何处,并识别那些会导致更新出错的初学者常见错误。
React旨在解决的问题
在组件库成为常规之前,交互式用户界面意味着需要使用命令式代码:先找到相关元素,然后在有变化时手动执行所有修改操作。对于一个简单的点赞按钮,其实现方式如下:
// Traditional DOM manipulation
const button = document.getElementById("like-button");
const countEl = document.getElementById("like-count");
const icon = document.getElementById("like-icon");
let liked = false;
let count = 42;
而点击处理函数则必须记住两种状态下的所有视觉细节:
button.addEventListener("click", function() {
if (!liked) {
liked = true;
count++;
countEl.textContent = count;
button.classList.add("liked");
button.classList.remove("unliked");
icon.src = "/icons/heart-filled.svg";
button.style.color = "#e0245e";
// trigger animation
button.classList.add("animate-pop");
setTimeout(() => button.classList.remove("animate-pop"), 300);
} else {
liked = false;
count--;
countEl.textContent = count;
button.classList.remove("liked");
button.classList.add("unliked");
icon.src = "/icons/heart-empty.svg";
button.style.color = "#6c757d";
}
});
对于单个按钮来说这还算可控。但现在想象一下有50篇帖子的动态内容,每篇帖子都有独立的点赞、评论、分享和保存控件,这些控件都可能因为用户点击或服务器发送的新数据而发生变化。这样一来,脚本就会变成由大量元素查找、监听器和变量构成的复杂网络,需要人工逐一维护。随着应用规模的扩大,会出现三种故障模式:
- 同步问题。DOM显示的内容与变量中的数据不一致。如果你更新了计数器元素却忘了修改变量,或者反之,那就需要调试两个不同的数据来源。
- 复用性差。处理函数与特定的元素ID及HTML结构绑定,因此将按钮移到其他页面时就必须重新编写代码。
- 复杂性高。每新增一个功能,都要求开发者了解它可能影响的其它部分;哪怕是微小的改动也可能破坏页面中其他功能。
2013年由Facebook开源的React通过一个理念解决了以上三个问题:不再直接发送DOM指令,而是描述在特定状态下应有的界面。React会将这个描述与屏幕上实际显示的内容进行对比,然后应用差异部分。由你描述界面,React负责更新。
JSX:可编译为 JavaScript 的标记语言
React 代码中最奇怪的一点就是在函数内部出现了类似 HTML 的标记:
function Greeting() {
return (
<div className="greeting">
<h1>Hello, Priya!</h1>
<p>Welcome back.</p>
</div>
);
}
那种标记就是 JSX,这是一种语法扩展,允许你在 JavaScript 文件中编写元素树结构。它既不是 HTML,也不是浏览器能够理解的格式。在代码实际运行之前,会通过构建步骤(如 Babel、TypeScript 编译器或使用它们的打包工具)将其重写为普通的函数调用。你需要这样编写:
// What you write (JSX):
return (
<h1 className="title">Hello!</h1>
);
然后编译器会生成与之等价的内容:
// What the compiler transforms it into:
return React.createElement("h1", { className: "title" }, "Hello!");
React.createElement仅返回一个描述该元素的普通对象,其中包含其类型、属性和子元素信息。React通过读取这些对象来决定实际的DOM应包含什么内容。(较新的JSX转换会使用react/jsx-runtime中的其他辅助函数,但生成的仍是类似的结构描述对象。)没人会手动编写这类调用代码;JSX的存在正是为了让嵌套的标记结构更易于阅读。
JSX与HTML的不同之处
其语法与HTML相近但并不完全相同。最容易让人混淆的差异包括:
// HTML JSX
// ───────────────────────────── ──────────────────────────────────
// class="title" className="title" ← JS reserved word
// for="email" htmlFor="email" ← JS reserved word
// <input> (self-closing optional) <input /> ← must self-close
// onclick="handler()" onClick={handler} ← camelCase, no quotes
// style="color: red" style={{ color: "red" }} ← JS object
在 JavaScript 中,class 和 for 是保留字,因此 JSX 使用 className 和 htmlFor。每个元素都必须被关闭。事件处理函数采用驼峰命名法,且接收的是函数而非字符串。内联样式以对象形式存在。如需深入了解这种语法背后的权衡,可参阅 为何 JSX 不是 HTML。
嵌入 JavaScript 表达式
大括号为 JavaScript 打开了一个应用窗口。任何能计算出值的表达式都可以放在其中:变量、函数调用、三元运算符、数组方法。在此示例中,首先计算在线状态:
function UserCard({ user }) {
const isOnline = user.lastSeen < Date.now() - 5 * 60 * 1000;
随后通过多个表达式来构建标记内容:截断后的个人简介、条件性的类名、条件性的标签以及格式化的日期:
return (
<div className="card">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio.length > 100 ? user.bio.slice(0, 100) + "..." : user.bio}</p>
<span className={isOnline ? "badge-green" : "badge-grey"}>
{isOnline ? "Online" : "Offline"}
</span>
<p>Joined: {new Date(user.joinedAt).toLocaleDateString()}</p>
</div>
);
}
规则很简单:大括号用于包裹表达式,而非语句。你可以在标记中直接使用三元运算符而不用if块,也可以使用.map()而不用for循环。
组件:返回UI的函数
组件是一种可重用的、自包含的UI元素,它以JavaScript函数的形式编写并返回JSX。这就是其完整定义:
// A component is just a function that returns JSX
function Button() {
return (
<button className="btn">
Click me
</button>
);
}
使用方式与HTML标签相同,而且可以随意添加多个:
function App() {
return (
<div>
<Button />
<Button />
<Button />
</div>
);
}
通过单个定义即可渲染出三个完全相同的按钮。
将小型组件组合成大型组件
当组件相互嵌套时,它们的功能会变得更加强大。那些小型、单一功能的组件就像积木一样被组合成更大的组件。一个头像组件只知道如何显示图片:
function Avatar({ src, alt }) {
return <img className="avatar" src={src} alt={alt} />;
}
在此基础上还有一系列稍大的组件:名称块、将头像与姓名结合在一起的用户信息行,以及将用户信息与帖子内容结合在一起的明信片:
function UserName({ name, handle }) {
return (
<div>
<strong>{name}</strong>
<span>@{handle}</span>
</div>
);
}function UserInfo({ user }) {
return (
<div className="user-info">
<Avatar src={user.avatar} alt={user.name} />
<UserName name={user.name} handle={user.handle} />
</div>
);
}function PostCard({ post, author }) {
return (
<article className="post-card">
<UserInfo user={author} />
<p>{post.content}</p>
<span>{post.likes} likes</span>
</article>
);
}
每一层都有其特定的功能。Avatar负责渲染图片,UserInfo则将头像与姓名并排显示,PostCard则负责构建整个帖子结构。这正是React所倡导的做法:将界面拆分成尽可能小的合理部分,为每个部分明确职责,再通过组合实现整体功能。关于构建可复用React组件的指南进一步阐述了组件设计的相关内容。
为何组件名称要大写
React 通过首字母来区分原生元素和组件。<button>会变成 DOM 按钮;而<Button>则会让 React 在作用域中查找名为Button的变量并调用它。以小写字母命名的组件会被视为未知的 HTML 标签,这往往是“我的组件没有渲染内容”这一问题的常见原因。
Props:从外部配置组件
一个始终显示“点击我”的按钮并没有太大用处。Props(即属性的缩写)是父组件向子组件传递数据的方式,因此一个定义可以适用于多种情况。
Props 的传递方向是单向的,从渲染另一个组件的组件传向下方被渲染的组件,并以单个对象参数的形式传递。父组件会像设置属性一样编写它们:
// Parent passes data as props (looks like HTML attributes)
function App() {
return (
<div>
<Button label="Submit" colour="blue" />
<Button label="Cancel" colour="grey" />
<Button label="Delete" colour="red" />
</div>
);
}
子组件会拆解出它所需的元素:
// Child receives them as an object
function Button({ label, colour }) {
return (
<button className={`btn btn-${colour}`}>
{label}
</button>
);
}
一个定义,三个外观不同的按钮,因为它们接收了不同的值。
属性可以携带任何类型的值
字符串只是开始而已。数字、布尔值、数组、对象和函数都是有效的属性。例如,产品卡片可以接收数据对象、回调函数以及标志位:
function ProductCard({ product, onAddToCart, featured }) {
return (
<div className={`card ${featured ? "card-featured" : ""}`}>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>₹{product.price.toLocaleString()}</p>
<p>{product.rating} ★ ({product.reviewCount} reviews)</p>
<button onClick={onAddToCart}>
Add to Cart
</button>
</div>
);
}
在调用处,这些值是通过大括号传递的:
// Usage:
<ProductCard
product={{ name: "Headphones", price: 2499, rating: 4.3, reviewCount: 128 }}
onAddToCart={() => addToCart(product.id)}
featured={true}
/>
请注意,内联的 onAddToCart 回调函数引用了 product.id,但这里的 product 只是作为属性传递的原始对象,并非作用域内的变量。在实际代码中,应使用在父组件中定义的变量,例如 onAddToCart={() => addToCart(item.id)}。将函数作为属性传递是子组件上报事件的标准方式,这一点在后面还会提到。
属性是只读的
组件绝不能修改自身的属性。在渲染过程中,这些属性都是固定的输入值。当需要更改时,应由父组件进行修改并使用新值重新渲染子组件。在子组件内部重新赋值属性是错误的做法:
// WRONG — never modify props
function Button({ count }) {
count = count + 1; // ← this is a mistake
return <button>{count}</button>;
}
重新赋值只会改变局部变量;它不会影响到父组件,且在下次渲染时就会消失。当组件需要可变化的值时,这就是状态存在的意义:
// CORRECT — props are read-only, use state for data that changes
function Button({ initialCount }) {
const [count, setCount] = useState(initialCount);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
在这里,initialCount仅用于初始化状态;在首次渲染之后,计数器就由该组件自行管理了。
状态:组件的私有内存
属性来自外部,而状态则是组件拥有并可随时间变化的数据。当状态发生变化时,React会使用新值重新渲染该组件,这样你就无需手动操作DOM了。
状态是通过从React导入的useState钩子来创建的:
import { useState } from "react";
计数器展示了其基本用法:
function Counter() {
const [count, setCount] = useState(0);
// ↑ current value ↑ function to update it ↑ initial value return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
useState(0)会返回一个包含当前值(count,初始值为0)和设置函数(setCount)的数组。通过设置函数传入新值,可以告知React将该值存储起来并重新渲染组件。
利用状态重建点赞按钮
最初那种命令式的点赞按钮现在变得简洁多了。首先进行导入操作:
import { useState } from "react";
接着需要为liked和count的任意组合描述对应的按钮样式:
function LikeButton({ initialLikes }) {
const [liked, setLiked] = useState(false);
const [count, setCount] = useState(initialLikes); function handleClick() {
if (liked) {
setLiked(false);
setCount(c => c - 1);
} else {
setLiked(true);
setCount(c => c + 1);
}
} return (
<button
onClick={handleClick}
className={liked ? "btn-liked" : "btn-default"}
>
{liked ? "♥" : "♡"} {count}
</button>
);
}
这里没有元素查找操作,也没有classList调用或textContent赋值。处理函数仅负责更新两个状态值,而JSX则根据这些值决定按钮的显示样式。React会确保DOM与组件状态保持一致。
注意更新器函数 setCount(c => c - 1)。传递函数而非数值可以获取最新状态,这在同一事件中有多个更新操作时更为安全。
每个实例都保留自己的状态
状态属于组件的特定实例,而非组件定义本身。渲染三个类似按钮时,每个按钮都有独立的 liked 和 count 值;点击其中一个不会影响其他按钮:
function Feed() {
return (
<div>
<LikeButton initialLikes={24} /> {/* has its own state */}
<LikeButton initialLikes={7} /> {/* has its own state */}
<LikeButton initialLikes={156} /> {/* has its own state */}
</div>
);
}
哪些内容应存储在状态中
以下类型的值适合使用状态来保存:
- 由于用户交互或外部数据输入而随时间变化的值
- 一旦发生变化就需要更新用户界面的值
- 属于该特定组件实例的值
避免将那些可以通过现有属性或状态计算得出的值(应在渲染时进行计算),以及那些无需重新渲染就会变化的值放入状态中,比如计时器ID,这类值更适合放在ref中。
重新渲染的原理
重新渲染是整个React模型背后的核心机制。当状态发生变化时,React会再次调用你的组件函数,获取最新的UI描述,将其与之前的描述进行比较,然后仅更新DOM中有所差异的部分。
在首次渲染时,计数器会生成相应的标记,React则会创建对应的DOM节点:
Initial render:
count = 0
Component runs → returns <p>Count: 0</p> <button>Increment</button>
React creates DOM nodes
当用户点击时,设置函数会触发新的渲染,React随后会比较两次得到的描述:
User clicks Increment:
setCount(1) called
React re-renders the component
count = 1
Component runs again → returns <p>Count: 1</p> <button>Increment</button>
React compares: <p>Count: 0</p> vs <p>Count: 1</p>
React updates only the text node inside <p>
Button is unchanged — React leaves it alone
在真实的 DOM 中,只有段落内的文本会发生变化,按钮则保持不变。React 不会重新构建整个页面,而是计算出最少的更改内容并仅应用这些更改,因此频繁的重新渲染通常不会有问题。
什么会导致组件重新渲染
当组件的状态发生变化时,它就会再次渲染:
1. The component's own state changes (setCount, setUser, etc.)
↓
Component re-renders
在另外几种情况下,它也会再次渲染:
2. The component's props change (parent passes different values)
↓
Component re-renders3. A context the component uses changes
↓
Component re-renders4. The parent re-renders
↓
All children re-render (unless memoized)
实际上,“props 发生变化”和“父组件重新渲染”是从两个不同角度观察到的同一事件:子组件之所以能收到新的 props,只是因为其父组件重新渲染了。实际后果是,默认情况下,父组件的渲染会级联影响到所有子组件,无论这些子组件的 props 是否发生了变化,除非它们被加入了记忆化处理。
这种级联反应很少会成为问题。渲染只不过是生成对象的函数调用;真正耗时的部分在于操作真实的 DOM,而 React 会尽量将这一操作降到最低。大多数实际的性能问题都源于组件和状态的架构方式,而非渲染次数的多少。
虚拟 DOM 与协调机制
React 会为 UI 维护一个轻量级的 JavaScript 表示形式,通常称为虚拟 DOM。每次渲染都会生成一棵新的元素对象树,React 会通过一种名为协调机制的过程将其与之前的树进行对比。对比的结果就是需要执行的实际 DOM 操作列表。
你永远不会直接操作这一层,它属于实现细节。之所以重要,是因为在内存中比较普通对象的成本很低,而真正的 DOM 操作则相对缓慢。通过这种比较来处理每一次更新,可以让 React 批量处理并减少这些高成本的操作。
声明式 UI 与命令式 UI
React 是声明式的,理解这个词才是真正的思维转变。让我们比较两种渲染列表的方式。
命令式:列出步骤
命令式版本首先清空容器:
// Imperative: manual DOM manipulation
const list = document.getElementById("list");
list.innerHTML = ""; // clear it
然后手动创建、配置并添加每个项目:
items.forEach(item => {
const li = document.createElement("li");
li.textContent = item.name;
li.className = item.active ? "active" : "";
li.addEventListener("click", () => handleClick(item.id));
list.appendChild(li);
});
这段代码是一系列指令:创建这个节点,设置那个属性,附加这个监听器,将其插入到指定位置。你需要负责操作之前的状态、之后的状态以及中间的每一个步骤。
声明式:描述最终结果
声明式版本规定了对于任何items数组,列表应呈现的样式:
// Declarative: describe the desired output
function ItemList({ items, onItemClick }) {
return (
<ul>
{items.map(item => (
<li
key={item.id}
className={item.active ? "active" : ""}
onClick={() => onItemClick(item.id)}
>
{item.name}
</li>
))}
</ul>
);
}
不存在“先清空列表再重新构建”的操作。你只需描述目标状态,React会自行决定如何从当前的DOM结构过渡到该目标状态。key属性会在每次渲染时告诉React哪些元素是哪个,这样它就能移动、更新或删除相应的元素,而无需重新创建所有元素。
为何声明式风格适合UI开发
- 可预测性。在相同的属性和状态下,组件会渲染出相同的结果。你可以将界面视为数据的纯函数:
UI = f(state, props)。 - 减少记忆负担。你无需记住DOM当前的样式或需要进行的修改,只需描述当前的状态即可。
组件树与单向数据流
每个React应用都是一棵树。根组件通常为App,它会渲染子组件,而这些子组件又会继续渲染各自的子组件,从而形成与屏幕结构一致的层次结构:
App
├── Navbar
│ ├── Logo
│ ├── NavLinks
│ └── UserMenu
│ ├── Avatar
│ └── DropdownMenu
├── Dashboard
│ ├── Sidebar
│ │ ├── SidebarLink (×5)
│ │ └── UserStats
│ └── MainContent
│ ├── StatsRow
│ │ ├── StatCard (×4)
│ └── RecentActivity
│ ├── ActivityItem (×10)
└── Footer
数据自上而下传递
数据通过属性从父组件传递给子组件。组件无法直接访问兄弟组件或父组件的状态。正是这种单向数据流使得大型应用依然易于理解:
App (has user, notifications, theme)
│
├── Navbar (receives: user, notifications)
│ │
│ └── UserMenu (receives: user)
│ │
│ └── Avatar (receives: user.avatar, user.name)
│
└── Dashboard (receives: user, theme)
│
└── MainContent (receives: theme)
Avatar只能看到UserMenu传递给它的内容,而UserMenu只能看到Navbar传递给它的内容。没有任何信息会向外或向上泄露,因此当某个值出错时,只需沿着父组件链向上查找即可。
事件向上传递
层级较深的组件无法直接修改其父组件的状态,但可以调用父组件传递下来的函数。状态由父组件掌控:
// Parent owns the state and passes down both the value and the updater
function App() {
const [searchQuery, setSearchQuery] = useState("");
父组件会将值和设置函数同时传递给子组件;搜索框会在输入内容发生变化时调用该设置函数:
return (
<div>
<SearchBar
query={searchQuery}
onChange={setSearchQuery} {/* passes the setter as a prop */}
/>
<Results query={searchQuery} />
</div>
);
}// Child receives the updater and calls it on user input
function SearchBar({ query, onChange }) {
return (
<input
value={query}
onChange={e => onChange(e.target.value)} {/* calls parent's setter */}
placeholder="Search..."
/>
);
}
查询仅存在于 App 中。SearchBar 不存储任何数据,它会通过 onChange 属性上报每次按键操作。Results 通过属性获取更新后的查询内容并重新渲染。数据以属性的形式向下传递,事件则通过函数调用向上发送。(开头标签内的解释性注释仅用于阅读;在真实的 JSX 中,大括号形式的注释应放在子元素中,而在标签内部则应使用普通的 /* */ 注释形式或直接省略。)
导致 React 更新失效的错误
直接修改状态值
人们很容易产生直接修改状态对象后再将其传回的冲动:
// WRONG — mutating state directly
const [user, setUser] = useState({ name: "Priya", age: 28 });
下面的第一个 birthday 示例修改了对象并将同一个引用传递给设置器,因此界面不会更新。第二个示例则使用展开运算符创建了新对象,React 会将其视为一次变化:
function birthday() {
user.age = 29; // ← directly modifying the object
setUser(user); // ← same object reference — React sees no change
} // UI does NOT update// CORRECT — create a new object
function birthday() {
setUser({ ...user, age: user.age + 1 }); // ← new object, React detects change
}
React 是通过使用 Object.is 比较引用来判断状态是否发生变化的,而非检查内容。如果直接修改对象或数组并传回相同的引用,React 会认为没有发生任何变化,从而可能跳过渲染。
数组也遵循同样的规则。向现有数组添加元素的操作会失败:
// WRONG — mutating the array
const [items, setItems] = useState([1, 2, 3]);
items.push(4); // ← modifies in place
setItems(items); // ← same reference — no re-render
而展开为新数组的操作则可以正常工作:
// CORRECT — create a new array
setItems([...items, 4]); // ← spread creates a new array
混淆属性与状态
一个简单的测试就能区分二者。如果某个值来自父组件,那它就是属性且为只读的;如果组件自己拥有该值并会随时间改变它,那它就是状态。将永远不会变化的值放入 useState 中,正是混淆的体现:
// Wrong: using state for something that should be a prop
function UserCard() {
const [userName] = useState("Priya"); // ← why is this state? It never changes here
return <p>{userName}</p>;
}
如果数据由父组件控制,就应将其作为属性接收:
// Correct: static data the parent controls comes as a prop
function UserCard({ name }) {
return <p>{name}</p>;
}
存储本可计算出的值
并非所有发生变化的内容都需要独立的状态。在列表旁保留计数会重复存储信息:
// Wrong: storing derived data in state
const [items, setItems] = useState([...]);
const [itemCount, setItemCount] = useState(0); // ← why is this state?
现在,对items的任何修改都需要同步更新itemCount,一旦遗漏某次更新,两者就会不一致。在渲染时计算该值可以从结构上确保二者保持同步:
// Every time items changes, you have to remember to also update itemCount
// And if you forget once, they're out of sync// Correct: derive it during render
const [items, setItems] = useState([...]);
const itemCount = items.length; // ← just a variable — always in sync
功能过重的组件
一个组件同时负责渲染侧边栏、表格、表单以及多个模态框,这样的设计难以理解、测试和复用。一个实用的准则是:如果无法用一句话概括某个组件的功能,那就说明它的职责过多。
// Hard to maintain — does everything
function UserDashboard() {
// manages user state
// fetches orders
// handles filters
// manages pagination
// controls modal open/close
// renders sidebar
// renders order table
// renders filter controls
// renders pagination
// renders modal
return ( /* 200 lines of JSX */ );
}
按功能划分后,每个部分都会有明确的名称和职责:
// Better — clear single responsibilities
function UserDashboard() {
return (
<DashboardLayout>
<OrderFilters />
<OrderTable />
<OrderPagination />
<OrderDetailModal />
</DashboardLayout>
);
}
用组件构建应用
在编写代码前先确定边界
从设计开始,标记出各个组件。任何重复出现的元素都是组件;任何具有单一明确功能的元素也是组件。一起变化的元素应归为一组,独立变化的元素则应分开。对于产品列表页面,其设计结构如下:
Looking at the design:
[ Filter Bar ]
[ Product Card ][ Product Card ][ Product Card ]
[ Product Card ][ Product Card ][ Product Card ]
[ Pagination ]
可分解为以下树形结构:
Components:
ProductPage
├── FilterBar
│ ├── FilterGroup (×3)
│ └── SortDropdown
├── ProductGrid
│ └── ProductCard (×N)
└── Pagination
将状态放在最低层的公共父组件中
对于每个状态,找到树形结构中需要它的最低层组件,并将状态保存在该组件中:
If only ProductCard needs the "expanded" state → put it in ProductCard
If FilterBar and ProductGrid both need filters → put filters in ProductPage
If Pagination and ProductGrid both need currentPage → put it in ProductPage
当两个兄弟组件需要相同的数据时,将该状态移到它们最近的共享父组件中并向下传递。这被称为“向上提升状态”。将状态保持在尽可能低的位置,还能减少状态变化时需要重新渲染的树形部分范围。
先构建静态版本,再添加状态
从硬编码数据开始:无需使用 useState,也无需处理函数,仅通过属性驱动标记。确定布局后,找出哪些值会随时间变化,并仅为这些值添加状态。第一步是创建一个完全静态的卡片:
// Step 1: static — no state, hardcoded data
function ProductCard() {
return (
<div className="card">
<img src="/headphones.jpg" alt="Headphones" />
<h3>Wireless Headphones</h3>
<p>₹2,499</p>
<button>Add to Cart</button>
</div>
);
}
第二步用 product 属性替换硬编码的内容,第三步则为“已添加”确认信息添加少量状态,该状态会在两秒后自动重置:
// Step 2: accept props — still no state
function ProductCard({ product }) {
return (
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>₹{product.price.toLocaleString()}</p>
<button>Add to Cart</button>
</div>
);
}// Step 3: add state for interactivity
function ProductCard({ product, onAddToCart }) {
const [added, setAdded] = useState(false); function handleAdd() {
setAdded(true);
onAddToCart(product.id);
setTimeout(() => setAdded(false), 2000);
} return (
<div className="card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>₹{product.price.toLocaleString()}</p>
<button onClick={handleAdd} disabled={added}>
{added ? "Added ✓" : "Add to Cart"}
</button>
</div>
);
}
由于结构与交互性是分离的,每一步都可以单独轻松验证。
核心要点
整个模型的简述。React存在的理由:
Why React:
Plain JS DOM manipulation is hard to scale and maintain
React: describe the UI, let React handle DOM updates
以及上述每个概念的要点,从JSX到常见错误:
JSX:
HTML-like syntax in JavaScript — compiled to React.createElement()
{} embeds any JavaScript expression
Use className (not class), onClick (not onclick)Components:
Functions that return JSX
Capital letter names (Button, not button)
Reusable, composable building blocksProps:
Data passed from parent to child (like function arguments)
Read-only — a component never modifies its own props
Can be strings, numbers, objects, arrays, functionsState:
const [value, setValue] = useState(initialValue)
Data owned by a component that changes over time
Calling the setter triggers a re-render
Each component instance has its own stateRe-rendering:
Happens when state changes, props change, or parent re-renders
React diffs the virtual DOM and updates only what changed
Not expensive — surgical DOM updatesDeclarative:
Describe what the UI should look like
React figures out what changed and how to update the DOM
UI = f(state, props) — predictable, testableData flow:
Props flow down (parent → child)
Events flow up (child calls parent's function)
One-way data flow keeps the application predictableCommon mistakes:
Mutating state directly (use spread / new objects)
Storing derived values in state (compute them during render)
Overloading one component (split by responsibility)
- 组件即函数,属性是其参数,状态则是其内存。UI始终是当前状态的呈现。
- 重新渲染在设计上成本很低;React帮您省去的DOM操作才是耗时部分。在担心渲染次数之前,先妥善设计状态结构。
- 始终为设置函数提供新的对象和数组;React比较的是引用而非内容。
- 将状态放在最需要它的组件中,在渲染时推导出其他所有内容,并通过回调属性让事件向上传递。
上下文、效应、自定义钩子以及性能优化都是建立在这些理念之上的。只要牢固掌握组件、属性、状态和重新渲染,React的更高级功能就会成为您已理解模型的延伸,而非需要记忆的新规则。
相关阅读
- 避免因 JavaScript 引用变异导致的隐式状态错误 — 了解为何通过引用方式修改对象和数组会破坏 React 的重新渲染机制,为何展开运算符仅能实现浅层复制,以及如何安全地对状态进行深度克隆。
- 理解 React 自定义钩子:无需共享状态即可复用逻辑 — 了解什么是 React 自定义钩子,它们如何在不同组件之间提取和共享带状态的逻辑,以及在构建此类钩子时需要避免的常见错误。