跳到主要内容

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>
  );
}
结果
Loading...

组件组合​

组件可以嵌套使用,这使得我们可以构建复杂的 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>
  );
}
结果
Loading...

组合是 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>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

这种模式被称为"组合",是 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>
  );
}
结果
Loading...

使用逻辑与运算符​

当只需要在条件为真时渲染内容时,可以使用 &&:

实时编辑器
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>
  );
}
结果
Loading...

提前返回​

对于复杂的条件逻辑,可以使用提前返回:

实时编辑器
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>
  );
}
结果
Loading...

列表渲染​

使用 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>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

状态更新的特点​

  1. 异步更新:调用 setState 后,状态不会立即改变
  2. 批量更新:React 会将多个状态更新合并为一次渲染
  3. 不可变更新:应该创建新的状态对象,而不是直接修改现有状态
实时编辑器
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>
  );
}
结果
Loading...

组件生命周期概念​

虽然现代 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>
  );
}
结果
Loading...

组件通信模式​

父传子(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>
  );
}
结果
Loading...

兄弟组件通信​

兄弟组件之间通过共同的父组件进行通信:

子组件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: 'zhang@example.com' },
    { id: 2, name: '李四', email: 'li@example.com' },
  ]);
  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>
  );
}
结果
Loading...

高阶组件(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
      }}
    />
  );
}
结果
Loading...

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>
);
}

小结​

本章我们学习了:

  1. 组件概念:组件是 React UI 的构建块,本质上是返回 JSX 的函数
  2. 函数组件:现代 React 开发的首选方式,简洁直观
  3. Props:用于父组件向子组件传递数据,是只读的
  4. 组合:通过嵌套组件来构建复杂的 UI,使用 children 传递内容
  5. 条件渲染:使用 && 或三元运算符根据条件显示不同内容
  6. 列表渲染:使用 map 遍历数组生成元素列表,必须提供 key
  7. 状态基础:使用 useState 管理组件内部数据
  8. 生命周期概念:理解挂载、更新、卸载三个阶段
  9. 组件通信:父传子(Props)、子传父(回调函数)、兄弟组件通信
  10. 设计模式:容器组件与展示组件分离

练习​

  1. 创建一个显示学生信息的组件,包含头像、姓名、成绩
  2. 创建一个按钮组件,支持自定义文本、颜色和点击事件
  3. 创建一个卡片组件,包含标题、图片和描述,并支持"收藏"功能
  4. 创建一个文章列表组件,显示多篇文章,并支持展开/收起详情
  5. 实现一个搜索组件,包含输入框和结果列表,演示组件通信

参考资源​