灏天阁

什么是 React 复合组件

· Yin灏

如果你还没有接触过复合组件,那么阅读完本文就会对它有初步认识。

复合组件是 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 提供给其他子组件。

- Book Lists -