React 组件基础
本章节将介绍 React 组件的基础知识。组件是 React 的核心,它允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行单独思考。
什么是组件?
从概念上讲,组件就像 JavaScript 函数。它们接受任意入参(称为 "props"),并返回用于描述页面展示内容的 React 元素。
React 组件的核心思想是将复杂的 UI 分解为独立的、可复用的部分。每个组件负责自己的逻辑和展示,这样可以提高代码的可维护性和可测试性。
函数组件
函数组件是当前 React 开发的首选方式。它们简洁、易于理解,并且可以使用 React Hooks 来管理状态和副作用。
function Welcome() { return ( <div style={{ padding: '10px', backgroundColor: '#f0f9ff', borderRadius: '4px', border: '1px solid #bae6fd' }}> <h2 style={{ color: '#0369a1' }}>你好,React!</h2> <p>这是一个基本的函数组件。</p> </div> ); }
组件组合
组件可以嵌套使用,这使得我们可以构建复杂的 UI。
function Button() { return <button style={{ padding: '5px 10px' }}>点击我</button>; } function Card() { return ( <div style={{ border: '1px solid #ccc', padding: '15px', borderRadius: '8px' }}> <h3>组件卡片</h3> <p>我们可以在这里放入其他组件:</p> <Button /> </div> ); }
组合是 React 中复用代码的推荐方式。通过组合,你可以将复杂组件拆分成更小的组件,每个组件专注于单一职责。
组件的 Props
Props 是传递给组件的属性。它们是只读的,组件不应该修改自己的 props。
基本用法
function Greeting({ name, role = "访客" }) { return ( <div> <h3>你好, {name}!</h3> <p>身份: {role}</p> </div> ); } function App() { return ( <div style={{ display: 'flex', gap: '20px' }}> <Greeting name="张三" role="管理员" /> <Greeting name="李四" /> </div> ); }
Props 的特点
- 只读性:组件绝不能修改自己的 props。这是 React 单向数据流的基础。
- 默认值:可以使用解构赋值的默认值语法设置默认 props。
- 任意类型:props 可以是字符串、数字、对象、数组、函数等任何类型。
传递函数作为 Props
函数作为 props 是 React 中实现子组件向父组件通信的常用方式:
function ChildButton({ onClick, label }) { return ( <button onClick={onClick} style={{ padding: '8px 16px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px' }} > {label} </button> ); } function ParentCounter() { const [count, setCount] = useState(0); return ( <div style={{ padding: '10px' }}> <p>计数: <strong>{count}</strong></p> <ChildButton label="增加计数" onClick={() => setCount(c => c + 1)} /> </div> ); }
children Props
children 是一个特殊的 prop,用于传递组件的内容:
function Card({ title, children }) { return ( <div style={{ border: '1px solid #e2e8f0', borderRadius: '8px', padding: '16px', backgroundColor: '#fff' }}> <h3 style={{ margin: '0 0 12px 0', color: '#1e293b' }}>{title}</h3> <div style={{ color: '#64748b' }}> {children} </div> </div> ); } function App() { return ( <Card title="用户信息"> <p>姓名:张三</p> <p>年龄:25岁</p> </Card> ); }
这种模式被称为"组合",是 React 中复用组件逻辑的重要方式。
条件渲染
在组件中,你可以使用 JavaScript 逻辑来决定渲染什么。
使用条件运算符
function Status({ isOnline }) { return ( <div style={{ padding: '5px 10px', borderRadius: '20px', display: 'inline-block', backgroundColor: isOnline ? '#dcfce7' : '#fee2e2', color: isOnline ? '#166534' : '#991b1b' }}> {isOnline ? '● 在线' : '○ 离线'} </div> ); } function UserList() { return ( <div> <p>用户 A: <Status isOnline={true} /></p> <p>用户 B: <Status isOnline={false} /></p> </div> ); }
使用逻辑与运算符
当只需要在条件为真时渲染内容时,可以使用 &&:
function Notification({ messages }) { const unreadCount = messages.filter(m => !m.read).length; return ( <div style={{ padding: '10px' }}> <h4>通知中心</h4> {unreadCount > 0 && ( <span style={{ background: '#ef4444', color: 'white', padding: '2px 8px', borderRadius: '10px', fontSize: '12px' }}> {unreadCount} 条未读 </span> )} {unreadCount === 0 && <span style={{ color: '#64748b' }}>无新消息</span>} </div> ); } function App() { return ( <div> <Notification messages={[{read: false}, {read: true}, {read: false}]} /> <Notification messages={[{read: true}, {read: true}]} /> </div> ); }
提前返回
对于复杂的条件逻辑,可以使用提前返回:
function UserPanel({ user }) { // 加载中状态 if (!user) { return <div style={{ color: '#64748b' }}>加载中...</div>; } // 未登录状态 if (!user.isLoggedIn) { return <div style={{ color: '#dc2626' }}>请先登录</div>; } // 正常渲染 return ( <div style={{ padding: '10px', background: '#f0fdf4', borderRadius: '4px' }}> <p>欢迎回来,{user.name}!</p> <p>角色:{user.role}</p> </div> ); } function App() { return ( <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}> <UserPanel user={null} /> <UserPanel user={{ isLoggedIn: false }} /> <UserPanel user={{ isLoggedIn: true, name: '张三', role: '管理员' }} /> </div> ); }
列表渲染
使用 map() 函数来渲染组件列表。记住要提供唯一的 key。
基本列表
function JuiceList() { const items = [ { id: 1, name: '苹果汁', price: 15 }, { id: 2, name: '橙汁', price: 12 }, { id: 3, name: '西瓜汁', price: 10 }, ]; return ( <ul style={{ listStyle: 'none', padding: 0 }}> {items.map(item => ( <li key={item.id} style={{ padding: '8px 12px', margin: '4px 0', background: '#f8fafc', borderRadius: '4px', display: 'flex', justifyContent: 'space-between' }}> <span>{item.name}</span> <span style={{ color: '#059669' }}>¥{item.price}</span> </li> ))} </ul> ); }
Key 的重要性
Key 帮助 React 识别哪些元素发生了变化、添加或删除。一个好的 key 应该是:
- 唯一:在兄弟节点中唯一
- 稳定:不会在重新渲染时改变
- 有意义:最好使用数据中的 ID,而不是数组索引
// 推荐:使用唯一 ID
items.map(item => <Item key={item.id} {...item} />)
// 不推荐:使用索引(仅在项目没有 ID 且不会重新排序时使用)
items.map((item, index) => <Item key={index} {...item} />)
为什么索引作为 key 不好?当列表项被插入、删除或重新排序时,索引会改变,React 可能会错误地复用组件实例,导致 bug 和性能问题。
组件状态基础
虽然详细的状态管理在 Hooks 章节介绍,但理解组件状态的基本概念很重要。
什么是状态?
状态(State)是组件内部的、可以随时间变化的数据。与 props 不同,状态由组件自己管理。
使用 useState
function ToggleSwitch() { const [isOn, setIsOn] = useState(false); return ( <div onClick={() => setIsOn(!isOn)} style={{ display: 'inline-flex', alignItems: 'center', gap: '8px', cursor: 'pointer', userSelect: 'none' }} > <div style={{ width: '50px', height: '26px', borderRadius: '13px', backgroundColor: isOn ? '#22c55e' : '#d1d5db', position: 'relative', transition: 'background-color 0.2s' }}> <div style={{ width: '22px', height: '22px', borderRadius: '50%', backgroundColor: 'white', position: 'absolute', top: '2px', left: isOn ? '26px' : '2px', transition: 'left 0.2s', boxShadow: '0 1px 3px rgba(0,0,0,0.3)' }} /> </div> <span>{isOn ? '开启' : '关闭'}</span> </div> ); }
状态更新的特点
- 异步更新:调用
setState后,状态不会立即改变 - 批量更新:React 会将多个状态更新合并为一次渲染
- 不可变更新:应该创建新的状态对象,而不是直接修改现有状态
function CounterDemo() { const [count, setCount] = useState(0); const [history, setHistory] = useState([]); const increment = () => { // 正确:使用函数式更新获取最新状态 setCount(prev => { const newCount = prev + 1; setHistory(h => [...h, newCount]); return newCount; }); }; return ( <div style={{ padding: '10px' }}> <p>当前计数: <strong>{count}</strong></p> <p>历史记录: {history.join(', ') || '无'}</p> <button onClick={increment} style={{ padding: '5px 15px' }} > 增加 </button> </div> ); }
组件生命周期概念
虽然现代 React 推荐使用函数组件和 Hooks,但理解生命周期概念仍然很重要。生命周期描述了组件从创建到销毁的各个阶段。
组件的三个阶段
挂载阶段(Mounting)
↓ 组件被创建并插入 DOM
更新阶段(Updating)
↓ props 或 state 变化导致重新渲染
卸载阶段(Unmounting)
→ 组件从 DOM 中移除
使用 useEffect 对应生命周期
在函数组件中,useEffect 可以模拟类组件的生命周期:
function LifecycleDemo() { const [count, setCount] = useState(0); const [mounted, setMounted] = useState(true); if (!mounted) { return ( <button onClick={() => setMounted(true)}> 重新挂载组件 </button> ); } return ( <div style={{ padding: '15px', border: '1px solid #e2e8f0', borderRadius: '8px' }}> <LifecycleLogger count={count} /> <div style={{ marginTop: '10px', display: 'flex', gap: '10px' }}> <button onClick={() => setCount(c => c + 1)}> 更新 ({count}) </button> <button onClick={() => setMounted(false)}> 卸载组件 </button> </div> </div> ); } function LifecycleLogger({ count }) { const [logs, setLogs] = useState([]); useEffect(() => { // 挂载时执行 setLogs(l => [...l, '组件挂载']); console.log('组件挂载'); return () => { // 卸载时执行 console.log('组件卸载'); }; }, []); // 空数组表示只在挂载时执行 useEffect(() => { // 更新时执行 if (count > 0) { setLogs(l => [...l, `count 更新为 ${count}`]); } }, [count]); // count 变化时执行 return ( <div> <p style={{ fontWeight: 'bold' }}>生命周期日志:</p> <ul style={{ margin: 0, paddingLeft: '20px', fontSize: '14px', color: '#64748b' }}> {logs.slice(-5).map((log, i) => ( <li key={i}>{log}</li> ))} </ul> </div> ); }
组件通信模式
父传子(Props)
这是最基本的数据传递方式:
// 父组件传递数据给子组件
<ChildComponent name="张三" age={25} />
子传父(回调函数)
子组件通过调用父组件传递的回调函数来传递数据:
function SearchBar({ onSearch }) { const [query, setQuery] = useState(''); return ( <div style={{ display: 'flex', gap: '8px' }}> <input value={query} onChange={(e) => { setQuery(e.target.value); onSearch(e.target.value); // 调用父组件的回调 }} placeholder="搜索..." style={{ padding: '8px', flex: 1 }} /> </div> ); } function SearchApp() { const [results, setResults] = useState([]); const items = ['苹果', '香蕉', '橙子', '葡萄', '西瓜']; const handleSearch = (query) => { const filtered = items.filter(item => item.includes(query) ); setResults(filtered); }; return ( <div style={{ padding: '10px' }}> <SearchBar onSearch={handleSearch} /> <ul style={{ marginTop: '10px' }}> {results.map((item, i) => ( <li key={i}>{item}</li> ))} </ul> </div> ); }
兄弟组件通信
兄弟组件之间通过共同的父组件进行通信:
子组件A → 回调 → 父组件 → Props → 子组件B
组件设计模式
容器组件与展示组件
将逻辑和展示分离:
// 展示组件:只负责渲染 function UserCard({ user, onFollow }) { return ( <div style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '12px', background: '#f8fafc', borderRadius: '8px' }}> <div style={{ width: '48px', height: '48px', borderRadius: '50%', background: '#3b82f6', color: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '20px' }}> {user.name[0]} </div> <div style={{ flex: 1 }}> <div style={{ fontWeight: 'bold' }}>{user.name}</div> <div style={{ fontSize: '14px', color: '#64748b' }}>{user.email}</div> </div> <button onClick={() => onFollow(user.id)} style={{ padding: '6px 12px' }} > 关注 </button> </div> ); } // 容器组件:负责逻辑 function UserListContainer() { const [users, setUsers] = useState([ { id: 1, name: '张三', email: '[email protected]' }, { id: 2, name: '李四', email: '[email protected]' }, ]); const [following, setFollowing] = useState([]); const handleFollow = (userId) => { setFollowing(prev => prev.includes(userId) ? prev.filter(id => id !== userId) : [...prev, userId] ); }; return ( <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> {users.map(user => ( <UserCard key={user.id} user={user} onFollow={handleFollow} /> ))} <p style={{ fontSize: '14px', color: '#64748b' }}> 已关注: {following.length} 人 </p> </div> ); }
高阶组件(HOC)
高阶组件是一个函数,接收一个组件并返回一个新组件。用于复用组件逻辑。
// 一个简单的 HOC 示例:添加加载状态
function withLoading(WrappedComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div style={{ padding: '20px', color: '#64748b' }}>加载中...</div>;
}
return <WrappedComponent {...props} />;
};
}
注意:在现代 React 中,自定义 Hook 通常是更好的选择。
ref 作为 Prop(React 19)
在 React 19 之前,函数组件需要使用 forwardRef 来接收 ref。从 React 19 开始,ref 可以像普通 prop 一样直接传递给函数组件,不再需要 forwardRef。
旧方式:使用 forwardRef
// React 18 及之前
import { forwardRef } from "react";
const MyInput = forwardRef(function MyInput({ placeholder }, ref) {
return <input ref={ref} placeholder={placeholder} />;
});
// 使用
<MyInput ref={inputRef} placeholder="请输入..." />
新方式:ref 作为 Prop(React 19)
// React 19 新语法
function MyInput({ placeholder, ref }) {
return <input ref={ref} placeholder={placeholder} />;
}
// 使用方式不变
<MyInput ref={inputRef} placeholder="请输入..." />
这大大简化了组件的编写,特别是对于那些需要暴露 DOM 引用的组件。
实际示例
function FormWithFocus() { const inputRef = useRef(null); const focusInput = () => { inputRef.current?.focus(); }; return ( <div style={{ padding: '15px', display: 'flex', flexDirection: 'column', gap: '10px' }}> {/* 直接传递 ref 给自定义组件 */} <CustomInput ref={inputRef} placeholder="点击按钮聚焦这个输入框" style={{ padding: '8px', fontSize: '16px' }} /> <button onClick={focusInput} style={{ padding: '8px 16px', alignSelf: 'flex-start' }} > 聚焦输入框 </button> </div> ); } // React 19:ref 作为普通 prop 接收 function CustomInput({ placeholder, style, ref }) { return ( <input ref={ref} placeholder={placeholder} style={{ padding: '8px 12px', fontSize: '16px', border: '2px solid #e2e8f0', borderRadius: '6px', outline: 'none', transition: 'border-color 0.2s', ...style }} /> ); }
ref 清理函数(React 19)
React 19 还新增了 ref 回调函数支持返回清理函数的能力:
function MeasureExample() {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
return (
<div
ref={(node) => {
if (node) {
// 测量元素尺寸
const rect = node.getBoundingClientRect();
setDimensions({ width: rect.width, height: rect.height });
}
// 新特性:返回清理函数
return () => {
console.log('元素被移除');
};
}}
style={{ padding: '20px', background: '#f0fdf4' }}
>
尺寸: {dimensions.width.toFixed(0)} x {dimensions.height.toFixed(0)}
</div>
);
}
迁移建议
如果你使用 TypeScript,需要注意 ref 回调函数的返回类型变化:
// TypeScript 类型变化
// 旧写法(可能返回元素实例)
<div ref={current => (instance = current)} />
// 新写法(不应该返回值,除非是清理函数)
<div ref={current => { instance = current }} />
// 如果需要清理
<div ref={current => {
instance = current;
return () => { /* 清理逻辑 */ };
}} />
React 官方提供了 codemod 工具来自动更新代码:
npx codemod@latest react/19/no-implicit-ref-callback-return
向后兼容
forwardRef仍然可用,但未来版本会被弃用- 新代码推荐直接使用 ref 作为 prop
- 使用 TypeScript 的项目需要更新类型定义
注意事项与最佳实践
1. 组件名必须大写开头
React 会将小写开头的标签视为 HTML 原生标签(如 <div>),大写开头的视为自定义组件。
// 正确
function MyComponent() { return <div>Hello</div>; }
// 错误 - React 会将其视为 HTML 标签
function myComponent() { return <div>Hello</div>; }
2. 保持组件纯函数
组件应该像纯函数一样,不应该修改它们接收到的 props。同样的输入应该产生同样的输出。
// 正确
function Greeting({ name }) {
return <h1>Hello, {name}</h1>;
}
// 错误 - 修改了 props
function Greeting({ name }) {
name = name.toUpperCase(); // 不要这样做!
return <h1>Hello, {name}</h1>;
}
3. 单个根元素
组件必须返回单个根元素。如果不想引入多余的 DOM 节点,可以使用 Fragment (<>...</>)。
// 正确
function Component() {
return <div>内容</div>;
}
// 错误:不能返回多个根元素
// function Component() {
// return <div>1</div><div>2</div>;
// }
// 解决:使用 Fragment
function Component() {
return (
<>
<div>1</div>
<div>2</div>
</>
);
}
4. 保持组件简洁
遵循单一职责原则,一个组件只做一件事。
// 好的写法:单一职责
function UserInfo({ user }) {
return (
<div>
<UserAvatar avatar={user.avatar} />
<UserName name={user.name} />
<UserBio bio={user.bio} />
</div>
);
}
5. 避免在渲染中创建函数
在渲染中创建函数会导致每次渲染都创建新函数,可能影响性能:
// 不好的做法:每次渲染都创建新函数
function BadExample({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => console.log(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
// 好的做法:使用 useCallback 或提取组件
function GoodExample({ items }) {
const handleClick = useCallback((id) => {
console.log(id);
}, []);
return (
<ul>
{items.map(item => (
<Item key={item.id} item={item} onClick={handleClick} />
))}
</ul>
);
}
function Item({ item, onClick }) {
return (
<li onClick={() => onClick(item.id)}>
{item.name}
</li>
);
}
小结
本章我们学习了:
- 组件概念:组件是 React UI 的构建块,本质上是返回 JSX 的函数
- 函数组件:现代 React 开发的首选方式,简洁直观
- Props:用于父组件向子组件传递数据,是只读的
- 组合:通过嵌套组件来构建复杂的 UI,使用 children 传递内容
- 条件渲染:使用
&&或三元运算符根据条件显示不同内容 - 列表渲染:使用
map遍历数组生成元素列表,必须提供key - 状态基础:使用 useState 管理组件内部数据
- 生命周期概念:理解挂载、更新、卸载三个阶段
- 组件通信:父传子(Props)、子传父(回调函数)、兄弟组件通信
- 设计模式:容器组件与展示组件分离
练习
- 创建一个显示学生信息的组件,包含头像、姓名、成绩
- 创建一个按钮组件,支持自定义文本、颜色和点击事件
- 创建一个卡片组件,包含标题、图片和描述,并支持"收藏"功能
- 创建一个文章列表组件,显示多篇文章,并支持展开/收起详情
- 实现一个搜索组件,包含输入框和结果列表,演示组件通信