本文参考 Patterns.dev
Patterns.dev 是一个免费的在线资源,提供了使用原生 JavaScript 或现代框架(
Vue
、React
)构建功能强大的 Web 应用程序的设计、渲染和性能模式
设计模式
复合模式( Compound Pattern )
在我们的应用程序中, 经常包含相互关联的组件, 它们通过共享状态相互依赖, 并共享相关逻辑。你可以在像选择、下拉组件或菜单项这样的组件中经常看到这一点。
复合组件模式允许你创建一同工作以完成需求的组件。
我们来看一个例子,这里我构建了一个Counter计数器组件,并在App.js中使用它:
jsx
import Counter from "./Counter";
export default function App() {
return (
<div>
<h1>Compound Component Pattern</h1>
<Counter
iconIncrease="+"
iconDecrease="-"
label="My NOT so flexible counter"
hideLabel={false}
hideIncrease={false}
hideDecrease={false}
/>
</div>
);
}
可以看到, 这个Counter组件接收了大量的props用来控制内部元素的显示与隐藏, 且组件不是很灵活, 比如, 我们想更改label
元素的位置, 就只能从Counter组件内部修改,但此时所有使用Counter组件的label
元素位置都将发生变化
现在让我们使用复合模式重构一下:
复合模式是使用React的Context API
实现的, 我们需要使用Context API
共享Counter组件中的状态, 编写新的组件并将其添加到Counter组件上。
第一步:创建Context和父组件,并提供子组件所需状态
jsx
import { createContext, useContext, useState } from "react";
const CounterContext = createContext();
export default function Counter({ children }) {
const [count, setCount] = useState(0);
const increase = () => setCount((c) => c + 1);
const decrease = () => setCount((c) => c - 1);
return (
<CounterContext.Provider value={{ count, increase, decrease }}>
<span>{children}</span>
</CounterContext.Provider>
);
}
第二步:创建子组件用于实现通用功能
jsx
function Label({ children }) {
return <span>{children}</span>;
}
function Count() {
const { count } = useContext(CounterContext);
return <span>{count}</span>;
}
function Increase({ icon }) {
const { increase } = useContext(CounterContext);
return <button onClick={increase}>{icon}</button>;
}
function Decrease({ icon }) {
const { decrease } = useContext(CounterContext);
return <button onClick={decrease}>{icon}</button>;
}
第三步:将子组件作为属性添加到父组件上
jsx
Counter.Label = Label;
Counter.Count = Count;
Counter.Increase = Increase;
Counter.Decrease = Decrease;
此时, 我们便可以灵活的使用Counter组件了
jsx
import Counter from "./Counter";
import "./styles.css";
export default function App() {
return (
<div>
<h1>Compound Component Pattern</h1>
<Counter>
<Counter.Label>My super flexible counter</Counter.Label>
<Counter.Decrease icon="-" />
<Counter.Count />
<Counter.Increase icon="+" />
</Counter>
</div>
);
}
我们可以轻松选择使用哪些子组件, 也能灵活的调换子组件的位置, 且只需要引入Counter组件即可。
完整源码请点击这里
当你在构建组件库时,复合模式非常有用 。在使用像 Semantic UI 这样的 UI 库时,你经常会看到这种模式。
高阶组件模式( HOC Pattern )
在我们的应用程序中,我们经常希望在多个组件中使用相同的逻辑。这种逻辑可以包括对组件应用特定样式、要求授权或添加全局状态等。
高阶组件(HOC)是一个接收另一个组件的组件。HOC 包含我们想要应用到作为参数传递的组件上的某些逻辑。应用该逻辑后,HOC 返回具有额外逻辑的元素。
假设我们总是想要在应用程序中的多个组件上添加某种特定样式。我们可以简单地创建一个 HOC,它将样式对象添加到我们传递给它的组件,而不是每次都在本地创建一个样式对象。
我们来看一个例子,我们现在有一个不可更改的ProductList
组件用来展示每个产品
jsx
function ProductItem({ product }) {
return (
<li className="product">
<p className="product-name">{product.productName}</p>
<p className="product-price">${product.price}</p>
<p className="product-description">{product.description}</p>
</li>
);
}
// 假设该组件不可更改
function ProductList({ title, items }) {
return (
<ul className="list">
{items.map((product) => (
<ProductItem key={product.productName} product={product} />
))}
</ul>
);
}
现在我们又有了新的需求:
- 一个按钮用来控制列表是否展示
- 一个按钮用来控制列表折叠(只显示三条)
- 展示产品的标题
此时我们便可以使用高阶组件,在不修改ProductList
组件的情况下完成这三个需求
jsx
import { useState } from "react";
//高阶组件通常以 withXXX开头
export default function withToggles(WrappedComponent) {
return function List(props) {
// 控制展示
const [isOpen, setIsOpen] = useState(true);
// 控制折叠
const [isCollapsed, setIsCollapsed] = useState(false);
const displayItems = isCollapsed ? props.items.slice(0, 3) : props.items;
function toggleOpen() {
setIsOpen((isOpen) => !isOpen);
setIsCollapsed(false);
}
return (
<div className="list-container">
<div className="heading">
// 展示标题
<h2>{props.title}</h2>
<button onClick={toggleOpen}>
{isOpen ? <span>∨</span> : <span>∧</span>}
</button>
</div>
{isOpen && <WrappedComponent {...props} items={displayItems} />}
<button onClick={() => setIsCollapsed((isCollapsed) => !isCollapsed)}>
{isCollapsed ? `Show all ${props.items.length}` : "Show less"}
</button>
</div>
);
};
}
使用时,我们将要添加功能的组件作为参数传入高阶组件中
js
import withToggle from "./HOC.js";
const ProductListWithToggles = withToggle(ProductList);
export default function App() {
return (
<div>
<h1>Render Props Demo</h1>
<div className="col-2">
<ProductListWithToggles
title="ProductListWithToggle"
items={products}
/>
</div>
</div>
);
}
高阶组件模式允许我们向多个组件提供相同的逻辑,同时将所有逻辑保持在一个地方。
渲染属性模式( Render Props Pattern )
渲染属性是组件上的一个属性,其值是一个返回 JSX 元素的函数。除了渲染属性外,组件本身不渲染任何内容。相反,组件简单地调用渲染属性,而不是实现自己的渲染逻辑。
我们直接来看一个例子:我们现在有一个List
组件, 它为列表提供了两个功能
- 控制列表显示与否
- 控制列表展开(所有)与显示部分(3条)
现在我们想复用这个组件展示ProductItem
和CompanyItem
, 这时我们便可以使用render prop
去代替之前的渲染逻辑
jsx
function List({ title, items, render }) {
const [isOpen, setIsOpen] = useState(true);
const [isCollapsed, setIsCollapsed] = useState(false);
const displayItems = isCollapsed ? items.slice(0, 3) : items;
function toggleOpen() {
setIsOpen((isOpen) => !isOpen);
setIsCollapsed(false);
}
return (
<div className="list-container">
<div className="heading">
<h2>{title}</h2>
<button onClick={toggleOpen}>
{isOpen ? <span>∨</span> : <span>∧</span>}
</button>
</div>
// 渲染逻辑
{isOpen && <ul className="list">{displayItems.map(render)}</ul>}
<button onClick={() => setIsCollapsed((isCollapsed) => !isCollapsed)}>
{isCollapsed ? `Show all ${items.length}` : "Show less"}
</button>
</div>
);
}
然后只需在使用List
组件时传入对应的渲染逻辑到render
属性中即可
jsx
export default function App() {
return (
<div>
<h1>Render Props Demo</h1>
<div className="col-2">
<List
title="Products"
items={products}
render={(product) => (
<ProductItem key={product.productName} product={product} />
)}
/>
<List
title="CompanyItem"
items={companies}
render={(companie) => (
<CompanyItem
key={companie.companyName}
company={companie}
defaultVisibility={false}
/>
)}
/>
</div>
</div>
);
}