什么是 React 复合组件
如果你还没有接触过复合组件,那么阅读完本文就会对它有初步认识。
复合组件是 React 的一个高级模式,通常是由两个或两个以上的组件共同来实现某项功能。其中一个组件作为父组件,其余组件作为它的子组件,利用这种显式父子关系来共享隐式状态。
复合组件支持组件中的状态和逻辑的共享,可以帮助开发人员构建更直观和灵活性的 API,避免了在子组件间使用 props 进行通信。
复合组件可以类比 HTML 中的 <select>
和 <option>
:
<select>
<option value="value1">key1</option>
<option value="value2">key2</option>
<option value="value3">key3</option>
</select>
如果单独使用其中的一个标签,并不能实现正常的功能。此外,它们是一组非常棒的 API。如果不是这种复合组件式的 API(这是 HTML,而不是 JSX),它很可能是下面的样子:
<select options="key1:value1;key2:value2;key3:value3"></select>
在这种实现方式中,API 一点都不优雅,特别是考虑到它还要支持 disabled
等其它属性。因此,复合组件为我们提供了一种表达组件间关系的方法。
这其中的另一个重要概念是“隐式状态”。<select>
元素隐式存储了关于所选 <option>
的状态,并与它的子组件共享该状态,以便它们根据该状态来渲染自己。但这种状态的共享是隐式的,因为在 HTML 代码中没有任何途径可以访问这个状态(而且也不需要去访问)。
好了,让我们来看看一个真实的 React 组件,以进一步了解复合组件的原理。下面是 Reach UI 中的 <Menu />
组件 的一个示例,它提供复合组件 API:
unction App() {
return (
<Menu>
<MenuButton>
Actions <span aria-hidden>▾</span>
</MenuButton>
<MenuList>
<MenuItem onSelect={() => alert('Download')}>Download</MenuItem>
<MenuItem onSelect={() => alert('Copy')}>Create a Copy</MenuItem>
<MenuItem onSelect={() => alert('Delete')}>Delete</MenuItem>
</MenuList>
</Menu>
)
}
在这个例子中,<Menu>
封装了一些可以共享的隐式状态,而且 <MenuButton>
、<MenuList>
和<MenuItem>
组件都可以访问和操作这个状态,并且这些都是封装在组件内部的。这样可以让组件的 API 更加优雅。
那么如何实现这样的组件呢?通常有两种方法:第一种是对 children 使用 React.cloneElement
;第二种是使用 React context。在本篇文章中,主要介绍如何使用 React context 创建一组简单的复合组件。
接下来,我们以 <Toggle>
组件为例来介绍实现过程。<Toggle>
组件包含了 <ToggleOn>
、 <ToggleOff>
和 <ToggleButton />
三个组件,当 <ToggleButton>
被点击时,会根据当前状态来展示 <ToggleOn>
或 <ToggleOff>
中的内容。该组件的使用方式如下:
function App() {
return (
<Toggle onToggle={(on) => console.log(on)}>
<ToggleOn>The button is on</ToggleOn>
<ToggleOff>The button is off</ToggleOff>
<ToggleButton />
</Toggle>
);
}
具体效果如下:
接下来,我们来看一下使用 context 和 hooks 来实现 <Toggle>
的完整代码:
import * as React from "react";
// this switch implements a checkbox input and is not relevant for this example
import { Switch } from "../switch";
const ToggleContext = React.createContext();
function useEffectAfterMount(cb, dependencies) {
const justMounted = React.useRef(true);
React.useEffect(() => {
if (!justMounted.current) {
return cb();
}
justMounted.current = false;
}, dependencies);
}
function Toggle(props) {
const [on, setOn] = React.useState(false);
const toggle = React.useCallback(() => setOn((oldOn) => !oldOn), []);
useEffectAfterMount(() => {
props.onToggle(on);
}, [on]);
const value = React.useMemo(() => ({ on, toggle }), [on]);
return (
<ToggleContext.Provider value={value}>
{props.children}
</ToggleContext.Provider>
);
}
function useToggleContext() {
const context = React.useContext(ToggleContext);
if (!context) {
throw new Error(
`Toggle compound components cannot be rendered outside the Toggle component`
);
}
return context;
}
function ToggleOn({ children }) {
const { on } = useToggleContext();
return on ? children : null;
}
function ToggleOff({ children }) {
const { on } = useToggleContext();
return on ? null : children;
}
function ToggleButton(props) {
const { on, toggle } = useToggleContext();
return <Switch on={on} onClick={toggle} {...props} />;
}
在代码里,我们使用 React 创建了一个 context,用于存储和更新状态;而 <Toggle>
组件负责将这个 context 提供给其他子组件。