第六章 Component Patterns

  • Container and presentational components
  • Compound compoents
  • Render props pattern
  • Higher-order components

container and presentational components

容器和表述组件。就是将复杂组件的属性内容进行抽取成为一个新的组件。

(略)

Compound components

合成组件,就是将一系列组件一起工作。

(略)

Higher-order components

A higher-order component(HOC) 是一个函数组件,接收一个组件参数,返回该组件的增强版本。这样看起来没什么意义,下面通过一个例子withLoader组件来阐述。最终效果类似延迟加载动态圈。

Adding asynchronous data fetching

下面构造一份延迟数据来模拟真实的网络环境,

1
2
3
4
5
6
7
8
9
const wait = (ms: number): Promise<void> => {
return new Promise(resolve => setTimeout(resolve, ms));
};

export const getProduct = async (id: number): Promise<IProduct | null> => {
await wait(1000);
const foundProducts = products.filter(customer => customer.id === id);
return foundProducts.length === 0 ? null : foundProducts[0];
};

接着在原来的ProductPage页面导入getProduct函数,

1
import { getProduct, IProduct } from "./ProductsData";

ProductPage状态中加入一个新的属性loading,表示数据是否已经加载,

1
2
3
4
5
interface IState {
product?: IProduct;
added: boolean;
loading: boolean;
}

在构造函数中初始化状态属性,

1
2
3
4
5
6
7
public constructor(props: Props) {
super(props);
this.state = {
added: false,
loading: true
};
}

在组件加载时使用getProduct函数,

1
2
3
4
5
6
7
8
9
public async componentDidMount() {
if (this.props.match.params.id) {
const id: number = parseInt(this.props.match.params.id, 10);
const product = await getProduct(id);
if (product !== null) {
this.setState({ product, loading: false });
}
}
}

这里使用了await关键字异步调用getProduct。另外还要修改生命周期方法componentDidMount带上async关键字。

Implementing the withLoader HOC

我们将会创建withLoader加载组件,被用于指示组件处于繁忙状态。

  1. 创建一个新文件,withLoader.tsx,内容如下,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import * as React from "react";

interface IProps {
loading: boolean;
}

const withLoader = <P extends object>(
Component: React.ComponentType<P>
): React.SFC<P & IProps> => (props: P & IProps) =>
props.loading ? (
<div className="loader-overlay">
<div className="loader-circle-wrap">
<div className="loader-circle" />
</div>
</div>
) : (
<Component {...props} />
);

export default withLoader;

其中,

  • withLoader是一个函数,接收一个类型是P的组件
  • withLoader调用一个函数组件
  • 函数组件的属性定义是P & IProps,它是一个交集类型
  • 组件的所有属性会通过SFC传入,并带上一个新的属性loading
  • props被解构为一个loading变量,剩余的其它属性作为rest参数
  1. 添加加载转轮的CSS样式,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.loader-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: black;
opacity: 0.3;
z-index: 10004;
}

.loader-circle-wrap {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
height: 100px;
width: 100px;
margin: auto;
}

.loader-circle {
border: 4px solid #ffffff;
border-top: 4px solid #899091;
border-radius: 50%;
width: 100px;
height: 100px;
animation: loader-circle-spin 0.7s linear infinite;
}

Consuming the withLoader HOC

要消费这个高阶组件,只需要简单包装原来的组件即可。

原来的Product.tsx文件修改为,

1
2
3
4
5
import withLoader from "./withLoader";

...

export default withLoader(Product);

在引用的页面部分修改为,即ProductPage页面,

1
2
3
4
5
6
7
8
9
10
{product || this.state.loading ? (
<Product
loading={this.state.loading}
product={product}
inBasket={this.state.added}
onAddToBasket={this.handleAddClick}
/>
) : (
<p>Product not found!</p>
)}

修改原来Props的属性选项为可选的,

1
2
3
4
5
interface IState {
product?: IProduct;
added: boolean;
loading: boolean;
}

另外需要处理空值的情况,修改

1
2
3
4
5
6
7
8
9
10
11
const handleAddClick = () => {
props.onAddToBasket();
};
if (!product) {
return null;
}
return (
<React.Fragment>
...
</React.Fragment>
);

HOC非常适用于对原来组件的增强处理。比较常见的是React Router中使用了非常多这种HOC模式。React Router自身也实现了withRouter组件函数。