08 — 组件与 Props

这一章讲 React 中最重要的两个概念之一:组件(另一个是 State)。
学完这章你就能读懂项目里 60% 的代码。


1. 组件是什么?

1.1 一句话

组件 = 一个返回 JSX 的函数

1
2
3
4
// 这就是一个组件
function Hello() {
return <h1>你好</h1>;
}

1.2 用组件就像用 HTML 标签

1
2
3
4
5
6
7
8
9
function App() {
return (
<div>
<Hello /> {/* 使用组件:HTML 风格 */}
<Hello /> {/* 可以用多次 */}
<Hello />
</div>
);
}

1.3 项目里所有「组件」长什么样

1
2
3
4
5
6
7
8
9
// 命名导出(import 时用 { Sidebar })
export function Sidebar() {
return <aside className={styles.sidebar}>...</aside>;
}

// 默认导出(import 时随便命名)
export default function App() {
return <div>...</div>;
}

项目里两种都有:根组件用默认导出,其他组件用命名导出(方便测试和按需导入)。


2. Props — 组件的「输入参数」

2.1 一句话

Props = 父组件传给子组件的数据(单向:从父 → 子)。

类似函数参数:

1
2
3
4
5
6
7
8
9
10
11
// 普通函数
function greet(name) {
return `Hello, ${name}`;
}
greet('Alice'); // 'Hello, Alice'

// React 组件
function Greet({ name }) {
return <h1>Hello, {name}</h1>;
}
<Greet name="Alice" />

2.2 项目里最常见的 Props 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ChatItem.tsx(项目实际代码)
interface Props {
chat: Chat; // 必填:对象
active: boolean; // 必填:布尔
onSelect: (id: string) => void; // 必填:函数(事件回调)
onDelete?: (id: string) => void; // 可选:函数(带 ?)
}

export function ChatItem({ chat, active, onSelect, onDelete }: Props) {
// ↑ 解构 Props
return (
<div onClick={() => onSelect(chat.id)}>
<span>{chat.title}</span>
{onDelete && (
<button onClick={(e) => { e.stopPropagation(); onDelete(chat.id); }}>
删除
</button>
)}
</div>
);
}

父组件调用

1
2
3
4
5
6
7
8
9
10
// Sidebar.tsx
{chatList.map((c) => (
<ChatItem
key={c.id} {/* 列表必须有 key */}
chat={c} {/* 对象 */}
active={c.id === currentChatId} {/* 布尔 */}
onSelect={selectChat} {/* 函数引用 */}
onDelete={deleteChat} {/* 函数引用 */}
/>
))}

3. 详细解释 Props 的每个类型

3.1 基本数据类型

1
2
3
4
5
6
7
8
9
10
interface Props {
title: string; // 字符串
count: number; // 数字
active: boolean; // 布尔
// ...
}

// 使用
<Greeting title="你好" count={3} active={true} />
// 字符串字面量 JS 表达式 JS 表达式

⚠️ 注意:HTML 属性字符串不需要 {},但其他 JS 值(变量、数字、布尔)必须用 {}

3.2 对象作为 Props

1
2
3
4
5
6
interface Props {
chat: Chat; // 对象
}

<ChatItem chat={someChat} />
// ↑ 传变量用 {},传字符串字面量可以不用 {}

3.3 函数作为 Props(事件回调)

这是 React 最常用的 Props 模式

1
2
3
4
5
6
interface Props {
onSelect: (id: string) => void; // 接收 id,无返回值
}

<ChatItem onSelect={(id) => console.log(id)} /> // 传箭头函数
<ChatItem onSelect={handleSelect} /> // 传函数引用(项目里常用)

3.4 可选 Props

1
2
3
4
5
interface Props {
name: string; // 必填
age?: number; // 可选(没传就是 undefined)
onClose?: () => void; // 可选函数
}

使用:

1
2
3
<Component name="A" />                    // OK(可选字段不传)
<Component name="A" age={30} /> // OK
<Component name="A" onClose={() => {}} /> // OK

为什么函数常用可选? 因为有些组件不一定需要回调。

3.5 React 内置的特殊 Props

Props说明
children组件标签之间的内容
key列表渲染时用于识别元素
classNameCSS 类名
style内联样式对象
ref引用 DOM 元素(10 章 讲)
onClick事件处理器

children 是项目里几乎没怎么用,但很重要:

1
2
3
4
5
6
7
8
9
10
11
// 定义
function Card({ children }) {
return <div className="card">{children}</div>;
}

// 使用
<Card>
<h1>标题</h1>
<p>内容</p>
</Card>
// children = <h1>标题</h1><p>内容</p>

3.6 项目里 Props 的类型定义模式

项目里 Props 类型都定义在使用它的组件文件里,紧邻组件:

1
2
3
4
5
6
7
8
9
// ChatItem.tsx
interface Props {
chat: Chat;
active: boolean;
onSelect: (id: string) => void;
onDelete: (id: string) => void;
}

export function ChatItem({ chat, active, onSelect, onDelete }: Props) { ... }

不会单独建一个 types/Props.ts


4. 完整例子:父→子→孙 数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// GrandParent.tsx
function GrandParent() {
const message = '你好'; // 数据源头
return <Parent message={message} />;
}

// Parent.tsx
function Parent({ message }) {
return <Child message={message} />;
// ↑ 接收再传下去(透传)
}

// Child.tsx
function Child({ message }) {
return <p>{message}</p>;
}

调用栈

1
2
3
4
<GrandParent />
↓ 渲染 <Parent message="你好" />
↓ 渲染 <Child message="你好" />
↓ 渲染 <p>你好</p>

5. props 是「只读」的

1
2
3
4
5
6
function Child({ name }: Props) {
// ❌ 错的:不能直接修改 Props
name = '新名字';

return <p>{name}</p>;
}

如果需要修改数据怎么办?

  • 父组件传一个 set 函数 给子组件
  • 子组件调用 set 函数
  • 父组件的状态更新
  • 数据流回到子组件

这就是 React 的「单向数据流」原则。


6. 实战例子:实现删除按钮(项目里的简化版)

6.1 思路

1
2
3
4
5
6
7
8
父组件 (Sidebar)
├─ 拥有删除函数 deleteChat()
└─ 把 deleteChat 通过 Props 传给子组件 ChatItem

子组件 ChatItem
└─ 用户点击删除按钮 → 调用 onDelete(chat.id)

回到父组件,执行删除逻辑(弹出确认框 → 调 API → 更新列表)

6.2 父组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Sidebar() {
const chatList = useChatStore(s => s.chatList);
const deleteChat = useChatStore(s => s.deleteChat);
// ↑ 来自 Store 的「删除会话」函数

return (
<div className="list">
{chatList.map(c => (
<ChatItem
key={c.id}
chat={c}
active={c.id === currentChatId}
onSelect={selectChat}
onDelete={deleteChat} {/* 传下去 */}
/>
))}
</div>
);
}

6.3 子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function ChatItem({ chat, active, onSelect, onDelete }: Props) {
return (
<div onClick={() => onSelect(chat.id)}>
<span>{chat.title}</span>
{!chat.isNew && (
<span onClick={(e) => {
e.stopPropagation(); {/* 阻止冒泡到父的 onClick */}
onDelete(chat.id); {/* 调用父传下来的函数 */}
}}>
<Trash2 size={12} />
</span>
)}
</div>
);
}

关键点

  • e.stopPropagation():防止删除按钮的点击事件冒泡到外层(外层会触发选中)
  • 子组件自己实现删除逻辑,只是「通知父组件」

7. 项目里常用的 Props 模式总结

模式 1:渲染列表(map + key)

1
2
3
{chatList.map((c) => (
<ChatItem key={c.id} chat={c} active={...} onSelect={...} onDelete={...} />
))}

模式 2:事件回调(父传子)

1
2
<Button onClick={handleClick}>...</Button>
<Component onClose={() => setShow(false)} />

模式 3:开关(boolean prop)

1
<Message isLast={i === messages.length - 1} isSending={isSending} />

模式 4:受控组件(value + onChange)

1
2
<input value={text} onChange={(e) => setText(e.target.value)} />
<textarea value={inputMessage} onChange={(e) => setInput(e.target.value)} />

模式 5:通用配置对象

1
2
<AgentSelector />                       {/* 不传 props,从 Store 读 */}
<MessageList /> {/* 同上 */}

项目里很多组件不接 Props,直接从 Zustand Store 读数据。这是大型项目常见模式。


8. 项目里的组件调用关系图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
App
├── Sidebar (无 props)
│ └── ChatItem × N
│ ├── Props: { chat, active, onSelect, onDelete }
│ └── 父是 Sidebar

├── MessageList (无 props)
│ ├── EmptyState (无 props)
│ └── Message × N
│ ├── Props: { message, isLast, isSending }
│ ├── Timeline
│ ├── Markdown
│ ├── ReferenceList
│ ├── RecommendQuestions
│ └── PptDownload

├── InputArea (无 props)
│ ├── AgentSelector (无 props)
│ └── FilePreview (无 props)

├── ConfirmDialog (无 props)
└── ConnectionError (无 props)

注意

  • 顶层 AppSidebarMessageListInputArea不接 Props,从 Store 读
  • 子组件(ChatItemMessageTimeline 等)通过 Props 接收数据

9. 一段话总结

组件 = 返回 JSX 的函数。
Props = 父传子的参数(只读)。
父子通信:父组件传函数给子组件 → 子组件调用 → 数据流回到父组件。
项目里:顶层组件从 Zustand Store 读数据,子组件通过 Props 接收。


接下来

你现在已经能读懂组件之间怎么配合了。接下来是 React 另一半核心 — 09-State与useState.md:组件内部会变化的数据。