Skip to content
组件与 Props

组件与 Props

React 的核心是组件。每个组件是一个返回 JSX 的 JavaScript 函数,通过 props 接收外部输入。


组件的本质

// 组件 = 函数签名 props → JSX
function Greeting(props) {
  return <h1>Hello, {props.name}!</h1>;
}

// 等价于
const Greeting = ({ name }) => <h1>Hello, {name}!</h1>;

React 会在每次渲染时调用组件函数,并把新的 props 传入。组件函数必须是纯函数(给定相同 props,渲染相同 UI,无副作用)。


Props 的核心特性

1. 只读性(不可变)

function Bad({ user }) {
  user.name = 'Hacker';       // 🚨 不要修改 props
  return <div>{user.name}</div>;
}

function Good({ user }) {
  const [name, setName] = useState(user.name);  // ✅ 复制到 state
  return <input value={name} onChange={e => setName(e.target.value)} />;
}

2. 任意 JS 值

Props 可以传递任何 JavaScript 值:

<Tooltip
  content="保存"              // 基本类型
  position={{ x: 10, y: 5 }}  // 对象
  onConfirm={() => save()}    // 函数
  icon={<SaveIcon />}         // JSX / React Element
  roles={['admin', 'editor']} // 数组
/>

🚨 陷阱:以内联对象或内联函数作为 prop,每次渲染都会创建新引用,可能破坏子组件的 memo 优化。对于性能敏感的组件,使用 useMemo / useCallback 包裹。

3. children — 特殊的 Prop

// children 是组件标签之间的所有内容
function Card({ children, title }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

// 用法
<Card title="用户信息">
  <p>姓名张三</p>
  <p>邮箱zhang@example.com</p>
</Card>

💡 最佳实践children 支持**组合(Composition)**优于继承。几乎所有 UI 库都使用 children 构建灵活的布局组件。


组件组合模式

1. 容器/展示分离

// 容器组件:负责数据和逻辑
function UserListContainer() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetchUsers().then(setUsers);
  }, []);

  return <UserList users={users} />;
}

// 展示组件:只负责渲染
function UserList({ users }) {
  return (
    <ul>
      {users.map(u => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
}

2. 插槽(Slots)模式

function Layout({ header, sidebar, children }) {
  return (
    <div className="layout">
      <header>{header}</header>
      <aside>{sidebar}</aside>
      <main>{children}</main>
    </div>
  );
}

// 使用
<Layout header={<Navbar />} sidebar={<Sidebar />}>
  <Dashboard />
</Layout>

3. Render Props

function DataFetcher({ url, render }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(url).then(r => r.json()).then(setData);
  }, [url]);

  if (!data) return <div>Loading...</div>;
  return <>{render(data)}</>;
}

// 使用
<DataFetcher
  url="/api/users"
  render={users => <UserTable users={users} />}
/>

💡 最佳实践:Render Props 已被 Hooks 和自定义组件大幅取代。在需要复用有状态行为 + 自定义渲染时可以考虑,但优先使用自定义 Hook。

4. 高阶组件(HOC)

// 注入 loading 状态
function withLoading(WrappedComponent) {
  return function WithLoading({ isLoading, ...props }) {
    if (isLoading) return <Spinner />;
    return <WrappedComponent {...props} />;
  };
}

const UserProfileWithLoading = withLoading(UserProfile);

💡 最佳实践:HOC 是 React 旧时代的复用模式。现代 React 优先使用自定义 Hooks 解决同样的需求。


组件生命周期(函数组件视角)

React 函数组件没有"生命周期方法",但可以通过 Hooks 覆盖所有生命周期场景:

Class 生命周期 函数组件等价
componentDidMount useEffect(() => {}, [])
componentDidUpdate useEffect(() => {})useEffect(() => {}, [deps])
componentWillUnmount useEffect 返回的清理函数
shouldComponentUpdate React.memo
componentDidCatch 仍用 Error Boundary(class 组件)
function UserProfile({ userId }) {
  useEffect(() => {
    // componentDidMount + componentDidUpdate(依赖变化)
    fetchUser(userId);
    
    // componentWillUnmount
    return () => cancelPendingRequest();
  }, [userId]);
}

组件拆分原则

信号 拆分规则
单文件 > 150 行 考虑拆分
一个组件做很多事 提取子组件
多组件共享 UI 逻辑 提取自定义 Hook
多处复用同一 UI 片段 提取为通用组件
渲染与数据逻辑耦合 分离为容器 + 展示

💡 最佳实践:拆分组件时问自己三个问题 — “这个组件只做一件事吗?可以不用注释就理解吗?能在不改代码的情况下复用它吗?”


defaultProps vs 默认参数

// React 18+ 推荐:JavaScript 默认参数
function Button({ text = '确认', onClick, size = 'md' }) {
  return <button onClick={onClick} className={`btn-${size}`}>{text}</button>;
}

// 老式写法(不推荐)
Button.defaultProps = {
  text: '确认',
  size: 'md',
};

💡 最佳实践:函数组件用 JavaScript 默认参数。defaultProps 是 class 组件遗留,在函数组件上已标记为 deprecated。


受控 vs 非受控

这是 React 组件设计中的关键抉择:

// 受控组件:状态由父组件管理
<Modal open={isOpen} onClose={() => setIsOpen(false)} />

// 非受控组件:自己管理状态
<Modal defaultOpen={true} />

大多数情况下,受控组件更灵活、可预测。非受控组件适合快速开发或与第三方库集成。


🚨 常见陷阱

  1. 修改 props:props 是只读的,需要修改时用 state
  2. 忘记 key:列表渲染必须有稳定的 key,且 key 要在 map 的最外层
  3. 过度拆分:不要为了拆而拆;保持同一"责任"的代码在一起
  4. 内联对象/函数:频繁渲染的组件中作为 prop 传递内联对象会破坏 memo
  5. 组件命名不够语义化Item 太泛,UserListItem 更明确