Part 2 React 组件通信
约 1948 字大约 6 分钟
2025-06-30
1 Props
与 Vue 相同,父组件向子组件传值也是通过 Props 进行的。
1.1 Props 的基本使用
例如我们定义一个子组件NameItem
,它包含一个li
列表项用于展示名字。子组件函数的第一个参数接收全部的 Props 形成一个对象。
function NameItem(props) {
return <li>{props.name}</li>
}
如果需要的话,也可以直接解构。
function NameItem({ name }) {
return <li>{name}</li>
}
而在父组件中,我们直接把 Props 写在属性上,如name={item.name}
。
这里等号左侧的name
是要传入子组件的 Props 的名称,而{item.name}
为这个 Props 的值。
function App() {
const nameList = [
{ id: 1, name: "YOAKE" },
{ id: 2, name: "AJohn" },
{ id: 3, name: "Zephyr" },
]
return (
<>
<ul>
{nameList.map(item => (
<NameItem key={item.id} name={item.name} />
))}
</ul>
</>
)
}
Props 的只读性
Props 是一种单向数据流,你无法在子组件中修改 Props 的值。
function NameItem(props) {
props.name = "123"
return <li>{props.name}</li>
} // ERROR: Cannot assign to read only property 'name' of object '#<Object>'
1.2 带默认值的 Props
为避免空值问题,可以使用带默认值的 Props:
function NameItem({ name = "未设置姓名" }) {
return <li>{name}</li>
}
1.3 Props 的类型系统
Props 本质上是一个对象,因此我们可以逐条定义类型。
type NameItemProps = {
name: string
}
function NameItem({ name }: NameItemProps) {
return <li>{name}</li>
}
2 插槽
Vue 中的插槽可用于传入特定的 HTML 结构,React 与之类似。
2.1 插槽语法糖
设想这样一个场景:子组件中部分结构一致,而部分结构需要根据父组件展示出不同的样式。插槽就可以很好地实现。
首先看看子组件。子组件从父组件中接收一个 Props 并解构为一个ReactNode
children
,然后把children
插入进指定位置。
function List({ children }: { children: React.ReactNode }) {
const nameList = [
{ id: 1, name: "YOAKE" },
{ id: 2, name: "AJohn" },
{ id: 3, name: "Zephyr" },
]
return (
<>
{children}
<ul>
{nameList.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</>
)
}
而在父组件中,子组件List
采用双标签的形式,中间包裹一段 JSX 表达式。这段 JSX 表达式就是作为children
传入子组件的插槽中。
function App() {
return (
<>
<List>
<div>这是一个普通的 div 作为标题</div>
</List>
<List>
<h2>这是 h2 作为标题</h2>
</List>
<List>
<a href="www.google.com">这是一个链接作为标题</a>
</List>
</>
)
}
2.2 使用 Props 代替插槽
我们注意到,children
也是作为第一个参数(的一个属性)传入进子组件的,这就说明所谓的插槽也是一个 Props。因此 Props 不仅可以传递变量,也可以传递 JSX 表达式。
由此我们可以抛开插槽的概念,直接使用 Props 来实现。并且这还解决了一个问题:React 中的插槽是不具名的,子组件中只能有一个插槽。
使用 Props 就可以把多段 JSX 表达式插入到指定的位置:
type ListProps = {
header: React.ReactNode
footer: React.ReactNode
}
function List({ header, footer }: ListProps) {
const nameList = [
{ id: 1, name: "YOAKE" },
{ id: 2, name: "AJohn" },
{ id: 3, name: "Zephyr" },
]
return (
<>
{header}
<ul>
{nameList.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
{footer}
</>
)
}
function App() {
return (
<>
<List
header={<div>这是一个普通的 div 作为标题</div>}
footer={<div>这是一个普通的 div 作为 footer</div>}
/>
<List
header={<h2>这是一个 h2 作为标题</h2>}
footer={<h2>这是一个 h2 作为 footer</h2>}
/>
<List
header={<h3>这是一个 h3 作为标题</h3>}
footer={<h3>这是一个 h3 作为 footer</h3>}
/>
</>
)
}
3 自定义事件
无论是在 Vue 还是在 React 中,子组件都不能直接向父组件中传值,而要使用逻辑:父组件中定义操作函数,然后为子组件添加一个自定义事件,事件的处理函数就是这个操作函数;然后子组件触发该自定义事件,父组件接收并调用操作函数,传值得以完成。
如下,父组件中有一个列表,子组件按钮控制该列表的显隐。
type ControllerProps = {
onHandleShow: () => void
}
function Controller({ onHandleShow }: ControllerProps) {
return <button onClick={onHandleShow}>切换显示</button>
}
function App() {
const [isShowNameList, setIsShowNameList] = useState(false)
const handleShow = () => {
setIsShowNameList(!isShowNameList)
}
return (
<>
<Controller onHandleShow={handleShow} />
{isShowNameList && <NameList />}
</>
)
}
我们依然注意到,自定义事件onHandleShow
依然是作为第一个参数(的一个属性)传入子组件的,这说明函数也可以作为 Props。
4 Context
Context 类似于一个全局变量存储器。组件可以直接访问 Context 中的值,而无需通过 Props 逐层传递。
4.1 Context 的基本使用
暗色模式已经非常普遍。设想在这个页面中,如果不使用 Context 的话,切换一次亮暗色模式要经过无数次的 Props 传递,无论是对人还是对机器都是一个灾难。
假设我们有层层嵌套的四个组件:App、Page、Header、Button。当我点击按钮时,所有组件均完成一次亮暗色切换。使用 Props 我们要写三个自定义事件,而使用 Context 的话:
import React, { useContext } from "react"
const ThemeContext = React.createContext("light")
function Button() {
const theme = useContext(ThemeContext)
return (
<>
<div>Button 的主题为:{theme}</div>
<button>点击切换主题</button>
</>
)
}
function Header() {
const theme = useContext(ThemeContext)
return (
<>
<div>Header 的主题为:{theme}</div>
<ThemeContext.Provider value={theme}>
<Button />
</ThemeContext.Provider>
</>
)
}
function Page() {
const theme = useContext(ThemeContext)
return (
<>
<div>Page 的主题为:{theme}</div>
<ThemeContext.Provider value={theme}>
<Header />
</ThemeContext.Provider>
</>
)
}
function App() {
const theme = useContext(ThemeContext)
return (
<>
<div>App 的主题为:{theme}</div>
<ThemeContext.Provider value="light">
<Page />
</ThemeContext.Provider>
</>
)
}
export default App
总结起来就三步:
第一步,在全局声明一个 Context。
import { useContext } from "react"
const ThemeContext = React.createContext("light")
第二步,若某一组件(如Page
)想要获取值,就在这个组件外部用Provider
包裹。
<ThemeContext.Provider value="light">
<Page />
</ThemeContext.Provider>
第三步,在Page
组件内使用useContext
访问。
function Page() {
const theme = useContext(ThemeContext)
return <div>Page 的主题为:{theme}</div>
}
4.2 可变值的 Context
正如上文所说,很多时候我们并不向仅仅读取这个值,而是想改变值。这时候就要用到可变值的 Context。
首先当然是创建一个 Context,除了状态值以外,还应该包括一个修改状态的函数:
const ThemeContext = React.createContext({
theme: 'light', // 状态值
toggleTheme: () => {} // 修改函数,默认为空函数
})
然后在顶层组件中创建状态,并通过Provider
提供。
function App() {
const [theme, setTheme] = useState("light")
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === "light" ? "dark" : "light"))
}
return (
<>
<div>App 的主题为:{theme}</div>
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<Page />
</ThemeContext.Provider>
</>
)
}
最后在组件中使用useContext
解构出状态和修改函数即可。
function Page() {
const { theme, toggleTheme } = useContext(ThemeContext)
return (
<>
<div>Page 的主题为:{theme}</div>
<Header />
</>
)
}
useState
和useContext
的解构
useState
返回一个数组,应该使用[]
解构。
const [theme, setTheme] = useState("light")
useContext
返回一个对象,应该使用{}
解构。
const { theme, toggleTheme } = useContext(ThemeContext)
最终的实现应该是:
import React, { useContext, useState } from "react"
const ThemeContext = React.createContext({
theme: "light",
toggleTheme: () => {},
})
function Button() {
const { theme, toggleTheme } = useContext(ThemeContext)
return (
<>
<div>Button 的主题为:{theme}</div>
<button onClick={toggleTheme}>点击切换主题</button>
</>
)
}
function Header() {
const { theme } = useContext(ThemeContext)
return (
<>
<div>Header 的主题为:{theme}</div>
<Button />
</>
)
}
function Page() {
const { theme } = useContext(ThemeContext)
return (
<>
<div>Page 的主题为:{theme}</div>
<Header />
</>
)
}
function App() {
const [theme, setTheme] = useState("light")
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === "light" ? "dark" : "light"))
}
return (
<>
<div>App 的主题为:{theme}</div>
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<Page />
</ThemeContext.Provider>
</>
)
}
export default App
你可能已经注意到<ThemeContext.Provider>
只出现了顶层组件中,这是因为 React 会自动从最近的 Provider 获取值。因此只在 App 中包一次 Provider,所有子组件就能直接读取。
4.3 Context Provider 封装
假如我们的ThemeContext.Provider
想用在其他组件中,该如何复用逻辑呢?
ThemeContext.Provider
是一个 JSX 表达式,它包裹了另一个 JSX 表达式,这完全可以用插槽来实现。
我们先写一个带插槽的 Provider。
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState("light")
const toggleTheme = () =>
setTheme(prev => (prev === "light" ? "dark" : "light"))
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
然后应用在想要包裹的组件上:
function App() {
return (
<>
<ThemeProvider>
<Page />
</ThemeProvider>
</>
)
}
就能实现原本的功能。