Skip to content
第 145 / 250 章前端⏱ 10 分钟阅读

第 145 章:组件与 Props

学习目标

  • 理解函数组件与组件复用
  • 掌握 Props 的类型化定义
  • 学会 children、默认值、可选 props
  • 避免组件设计的常见反模式

一、组件定义

1.1 函数组件(推荐)

tsx
// 简单函数
function Welcome() {
  return <h1>欢迎</h1>;
}

// 带 props
interface WelcomeProps {
  name: string;
}
function Welcome({ name }: WelcomeProps) {
  return <h1>欢迎,{name}</h1>;
}

// 箭头函数
const Welcome = ({ name }: WelcomeProps) => <h1>欢迎,{name}</h1>;

1.2 使用组件

tsx
<Welcome name="张三" />
<Welcome name="李四" />

二、Props 详解

2.1 类型化 props

tsx
interface ButtonProps {
  label: string;              // 必填
  onClick?: () => void;       // 可选
  variant?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
  children?: React.ReactNode; // 插槽
}

function Button({
  label,
  onClick,
  variant = 'primary',
  disabled = false,
  children,
}: ButtonProps) {
  const cls = `btn btn-${variant} ${disabled ? 'opacity-50' : ''}`;
  return (
    <button className={cls} onClick={onClick} disabled={disabled}>
      {children ?? label}
    </button>
  );
}

2.2 children 插槽

tsx
<Button>点我</Button>
// 渲染:children = "点我",label 被忽略

<Button label="提交" />
// 渲染:label = "提交",children 是 undefined

2.3 任意 props

tsx
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
  title: string;
}

function Card({ title, children, ...rest }: CardProps) {
  return (
    <div {...rest}>
      <h2>{title}</h2>
      {children}
    </div>
  );
}

2.4 必填 vs 可选

tsx
interface Props {
  required: string;     // 必填
  optional?: number;    // 可选
}

三、组合模式

3.1 容器与子组件

tsx
function Card({ children }: { children: React.ReactNode }) {
  return <div className="rounded border p-4">{children}</div>;
}

function CardHeader({ title }: { title: string }) {
  return <h3 className="font-bold">{title}</h3>;
}

// 使用
<Card>
  <CardHeader title="标题" />
  <p>内容</p>
</Card>

3.2 渲染函数 props(Render Props)

tsx
interface ListProps<T> {
  items: T[];
  render: (item: T) => React.ReactNode;
}

function List<T>({ items, render }: ListProps<T>) {
  return <ul>{items.map(render)}</ul>;
}

// 使用
<List
  items={users}
  render={user => <li key={user.id}>{user.name}</li>}
/>

四、组件组合 vs 配置

tsx
// ❌ 配置式(难以维护)
<BigForm
  showAvatar
  showEmail
  showPhone
  avatarSize={50}
  emailRequired
  phoneRequired
/>

// ✅ 组合式(灵活)
<BigForm>
  <BigForm.Avatar size={50} />
  <BigForm.Email required />
  <BigForm.Phone required />
</BigForm>

五、Props 传递最佳实践

5.1 明确传值

tsx
// ✅ 显式
<UserCard name="Tom" age={18} />

// ❌ 展开对象(props 不清晰)
<UserCard {...user} />

5.2 避免太深嵌套

tsx
// ❌ 组件 A → B → C → D(prop drilling)
<Layout>
  <Sidebar>
    <Menu>
      <MenuItem user={user} />  // D 用了 user
    </Menu>
  </Sidebar>
</Layout>

// ✅ 用 Context / Zustand

5.3 反模式:不要修改 props

tsx
// ❌ 错误:props 是只读的
function Component({ items }: Props) {
  items.push('new');   // 直接修改 props,React 警告
}

// ✅ 复制后再改
function Component({ items }: Props) {
  const list = [...items, 'new'];
}

六、组件命名与导出

tsx
// 1. 文件名与组件名一致
// UserCard.tsx
export function UserCard() {}

// 2. 一个文件一个主组件(辅助组件可放同文件)
function UserCard() {}
function UserCardAvatar() {}  // 辅助
export { UserCard };

// 3. 默认导出 vs 具名导出
export default UserCard;       // 默认
export { UserCard };            // 具名(推荐)

七、本章小结

概念关键
函数组件function 或箭头函数
Props通过参数传入,只读
默认值解构时 = 'value'
childrenReactNode 类型
反模式不要修改 props,避免 prop drilling

动手练习

  1. 写一个 Avatar 组件,接受 srcsizealt props
  2. 写一个 Card 组件,接受 titlechildrenfooter
  3. 写一个泛型 List<T>,用 render prop 模式
  4. 重构一个 prop 过多的组件,用组合模式拆分

推荐阅读


下一章:第 146 章:State 与 useState

本站基于 VitePress 构建 · 由 Codebook 团队维护