一、引言
tsx
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
const increase = () => setCount((c) => c + 1);
const decrease = () => setCount((c) => c - 1);
return (
<>
<button onClick={increase}>-</button>
<span>{count}</span>
<button onClick={decrease}>+</button>
</>
);
}
export default Counter;
通过上述代码,我们可以轻松实现图中第一个样式的计数器。然而,如果需要让这个组件在不同的使用场景中呈现图中所示的其他几种样式,我们通常需要重新编写新的组件以适应不同的内容结构,或者增加复杂的布局样式来调整组件的元素布局。但事实上,这些组件在功能逻辑上是一致的,变化的仅仅是元素的布局位置。在这种情况下,我们所编写的组件的复用性就大打折扣了。那么,如何优化这一问题呢?本文将介绍复合组件模式(Compound Components Pattern),这是一种在特定应用场景下能够显著提升组件可塑性的模式,通过采用这种模式,所构建的组件将更加灵活和高效。
二、复合组件
复合组件模式是一种高级的组件设计策略,通过将多个组件融合成一个更高层次的超级组件来实现。这些组件在内部能够共享状态,并且支持彼此之间的通信。在 HTML 中,<select>
和<option>
就是典型的复合组件模式,当用户点击某个选项时,选择器能够自动识别知道是选中了哪个选项。类似地,在 React 中构建复合组件时,我们也会在组件内部共享状态,在外部不再需要显式地配置状态。以引言中的"计数器组件"为例,以下将详细介绍复合组件的编写和使用。
2.1 组件编写
复合组件的状态管理是在内部进行,其也有非常多的方案可供选择,例如可以在内部组件之间通过 props 来进行传递,或者通过 Context API 来进行管理。本文我们选择 Context API 作为状态管理方案来进行介绍,通过 Props 传递状态的方式可阅读《2.3 其他写法》。
第一步,我们需要先创建一个 Context,利用 Context API 的能力来实现状态管理,其可以将状态共享到内部其他子组件中;
tsx
const CounterContext = createContext({});
第二步,定义一个父组件并将其暴露给外部,父组件承担了管理复合组件的状态和逻辑的职责,并且通过 ContextProvider 可以将状态传递给其子组件;
tsx
function Counter({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
const increase = () => setCount((c) => c + 1);
const decrease = () => setCount((c) => c - 1);
return (
<CounterContext.Provider
value={{
count,
increase,
decrease,
}}
>
<div>{children}</div>
</CounterContext.Provider>
);
}
export default Counter;
第三步,创建在父组件中呈现的子组件,组件之间能够互相协同工作以达成某种功能,在计数器组件中,通过 Increase 和 Decrease 组件的点击行为能够更新数值能并在 Count 组件中体现。
tsx
function Count() {
const { count } = useContext(CounterContext);
return <span>{count}</span>;
}
function Increase({ children }: { children: ReactNode }) {
const { increase } = useContext(CounterContext);
return <span onClick={increase}>{children}</span>;
}
function Decrease({ children }: { children: ReactNode }) {
const { decrease } = useContext(CounterContext);
return <span onClick={decrease}>{children}</span>;
}
第四步,为了让这些子组件能够获取对 ContextProvider 提供的值,我们需要将这些子组件作为 Counter 父组件的一部分,当我们想在任何文件中使用计数器组件时,只需要引入 Counter 组件即可,无需再引入 Count/Increase/Decrease 这些组件了,至此完成了 Counter 复合组件的编写。
tsx
Counter.Count = Count;
Counter.Increase = Increase;
Counter.Decrease = Decrease;
完整代码如下:
tsx
import { ReactNode, createContext, useContext, useState } from "react";
// 1. Create a context
const CounterContext = createContext({
count: 0,
increase: () => {},
decrease: () => {},
});
// 2. Create parent component
function Counter({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
const increase = () => setCount((c) => c + 1);
const decrease = () => setCount((c) => c - 1);
return (
<CounterContext.Provider
value={{
count,
increase,
decrease,
}}
>
<div>{children}</div>
</CounterContext.Provider>
);
}
// 3. Create child components to help implementing the common task
function Count() {
const { count } = useContext(CounterContext);
return <span>{count}</span>;
}
function Increase({ children }: { children: ReactNode }) {
const { increase } = useContext(CounterContext);
return <span onClick={increase}>{children}</span>;
}
function Decrease({ children }: { children: ReactNode }) {
const { decrease } = useContext(CounterContext);
return <span onClick={decrease}>{children}</span>;
}
Counter.Count = Count;
Counter.Increase = Increase;
Counter.Decrease = Decrease;
export default Counter;
复合组件促进了父组件和子组件之间的关注点分离,使代码更易于维护和理解。在计数器组件中,Counter 组件作为父组件,管理了整个组件的状态和通信,利用 Context API 的能力通过 CounterProvider 来进行状态的共享和分发,最终在子组件中消费这些状态,也可以进行状态通信。
2.2 组件使用
tsx
// 只需导入 Counter 即可
import Counter from "./components/Counter";
// 样式1
<Counter>
<Counter.Decrease><button>-</button></Counter.Decrease>
<Counter.Count />
<Counter.Increase><button>+</button></Counter.Increase>
</Counter>
// 样式2
<Counter>
<Counter.Count />
<Counter.Decrease><button>-</button></Counter.Decrease>
<Counter.Increase><button>+</button></Counter.Increase>
</Counter>
// 样式3
<Counter>
<div className="flex-center">
<Counter.Count />
<div>
<Counter.Decrease><button>-</button></Counter.Decrease>
<Counter.Increase><button>+</button></Counter.Increase>
</div>
</div>
</Counter>
// 样式4
<Counter>
<div className="flex-center">
<Counter.Count />
<span className="flex-column">
<Counter.Decrease><button>-</button></Counter.Decrease>
<Counter.Increase><button>+</button></Counter.Increase>
</span>
</div>
</Counter>
通过复合组件模式创建的组件在使用上非常简洁,一般我们使用多个互相关联的组件时会把所有props
都塞进一个巨大的父组件中,然后再把这些props
层层传递递到子组件中去,但复合组件模式把每个props
都连接到各自最有意义的子组件上,并且在内部完成这些状态管理和通信。
2.3 其他写法
前面我们也提到状态管理还可以通过在内部组件之间通过props 传递的方式实现,我们通过遍历 children 并 clone 元素将状态值添加到 props 上去。一般不推荐该写法,灵活性低,组件嵌套受到限制,只有父组件的直接子组件才被添加了相关的状态。
tsx
import { Children, ReactNode, cloneElement, useState } from "react";
function Counter({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
const increase = () => setCount((c) => c + 1);
const decrease = () => setCount((c) => c - 1);
return <>{Children.map(children, (child) => cloneElement(child, { count, increase, decrease }))}</>;
}
function Count({ count }: { count: number }) {
return <span>{count}</span>;
}
function Increase({ children, increase }: { children: ReactNode; increase: () => void }) {
return <span onClick={() => increase()}>{children}</span>;
}
function Decrease({ children, decrease }: { children: ReactNode; decrease: () => void }) {
return <span onClick={() => decrease()}>{children}</span>;
}
Counter.Count = Count;
Counter.Increase = Increase;
Counter.Decrease = Decrease;
export default Counter;
三、总结
在 React 应用程序开发中,复合组件模式是一种强大的设计策略,它提供了一种高效的方式来共享状态,并且允许组件在自身内部独立地进行状态管理。此外,复合组件模式显著提升了应用程序的可维护性和可扩展性,它允许开发者自定义和配置单个子组件的行为与外观,而不影响整个复合组件的功能。总而言之,复合组件模式是React生态系统中的一项核心技术。它通过提供灵活的状态共享机制、增强组件的可重用性、提高代码的清晰度和可扩展性,以及支持自定义配置,极大地丰富了React应用程序的设计和开发体验。当面临需要多个组件协同工作,共享状态和逻辑处理的场景时,复合组件模式无疑是一个理想的选择。
One More Thing
在 React 开发中,除了复合组件模式,还有许多有意思的设计模式值得探索,例如 Render Props Pattern、Hooks Pattern、Container/Persentational Pattern 和 HOC Pattern 等。推荐访问网站 👉 patterns.dev,其不仅涵盖了 React 的各种设计模式,还提供了 JavaScript 和 Vue 等其他技术栈的内容。