跳到主要内容

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: '[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>
  );
}
结果
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. 实现一个搜索组件,包含输入框和结果列表,演示组件通信

参考资源