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
| export function Sidebar() { return <aside className={styles.sidebar}>...</aside>; }
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');
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
| interface Props { chat: Chat; active: boolean; onSelect: (id: string) => void; onDelete?: (id: string) => void; }
export function ChatItem({ chat, active, onSelect, onDelete }: 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
| {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} />
|
⚠️ 注意: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; }
<ChatItem onSelect={(id) => console.log(id)} /> <ChatItem onSelect={handleSelect} />
|
3.4 可选 Props
1 2 3 4 5
| interface Props { name: string; age?: number; onClose?: () => void; }
|
使用:
1 2 3
| <Component name="A" /> <Component name="A" age={30} /> <Component name="A" onClose={() => {}} />
|
为什么函数常用可选? 因为有些组件不一定需要回调。
3.5 React 内置的特殊 Props
| Props | 说明 |
|---|
children | 组件标签之间的内容 |
key | 列表渲染时用于识别元素 |
className | CSS 类名 |
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>
|
3.6 项目里 Props 的类型定义模式
项目里 Props 类型都定义在使用它的组件文件里,紧邻组件:
1 2 3 4 5 6 7 8 9
| 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
| function GrandParent() { const message = '你好'; return <Parent message={message} />; }
function Parent({ message }) { return <Child message={message} />; }
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) { 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);
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 /> {} <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)
|
注意:
- 顶层
App、Sidebar、MessageList、InputArea 等不接 Props,从 Store 读 - 子组件(
ChatItem、Message、Timeline 等)通过 Props 接收数据
9. 一段话总结
组件 = 返回 JSX 的函数。
Props = 父传子的参数(只读)。
父子通信:父组件传函数给子组件 → 子组件调用 → 数据流回到父组件。
项目里:顶层组件从 Zustand Store 读数据,子组件通过 Props 接收。
接下来
你现在已经能读懂组件之间怎么配合了。接下来是 React 另一半核心 — 09-State与useState.md:组件内部会变化的数据。