译者简介 zqlu 蚂蚁金服·数据体验技术团队javascript
翻译自Ultimate React Component Patterns with Typescript 2.8,做者Martin Hocheljava
这篇博客受React Component Patterns启发而写react
在线Demogit
若是你了解我,你就已经知道我不编写没有类型定义的javascript代码,因此我从0.9版本后,就很是喜欢TypeScript了。除了有类型的JS,我也很是喜欢React库,因此当把React和Typescript 结合在一块儿后,对我来讲就像置身天堂同样:)。整个应用程序和虚拟DOM中的完整的类型安全,是很是奇妙和开心的。github
因此这篇文章说是关于什么的呢?在互联网上有各类关于React组件模式的文章,但没有介绍如何将这些模式应用到Typescript中。此外,即将发布的TS 2.8版本带来了另人兴奋的新功能如、若有条件的类型(conditional types)、标准库中新预约义的条件类型、同态映射类型修饰符等等,这些新功能是咱们可以以类型安全的方式轻松地建立常见的React组件模式。typescript
这篇文章篇幅会比较长,因此请你坐下放轻松,与此同时你将掌握Typescript下的 终极React组件模式。shell
全部的模式/例子均使用typescript 2.8版本和strict modenpm
首先,咱们须要安装typescript和tslibs帮助程序库,以便咱们生出的代码更小json
yarn add -D typescript@next
# tslib 将仅用与您的编译目标不支持的功能
yarn add tslib
复制代码
有了这个,咱们能够初始化咱们的typescript配置:数组
# 这条命令将在咱们的工程中建立默认配置 tsconfig.json
yarn tsc --init
复制代码
如今咱们来安装 react、react-dom 和它们的类型定义。
yarn add react react-dom
yarn add -D @types/{react,react-dom}
复制代码
棒极啦!如今咱们能够开始进入咱们的组件模式吧,不是吗?
你猜到了,这些是没有state的组件(也被称为展现型组件)。在部分时候,它们也是纯函数组件。让咱们用TypeScript建立人造的无状态Button组件。
同使用原生JS同样,咱们须要引入React以便咱们可使用JSX
import React from 'react'
const Button = ({ onClick: handleClick, children }) => (
<button onClick={handleClick}>{children}</button>
)
复制代码
虽然 tsc 编译器如今还会跑出错误!咱们须要显式的告诉咱们的组件/函数咱们的props是什么类型的。让咱们定义咱们的 props:
import React, { MouseEvent, ReactNode } from 'react'
type Props = {
onClick(e: MouseEvent<HTMLElement>): void
children?: ReactNode
}
const Button = ({ onClick: handleClick, children }: Props) => (
<button onClick={handleClick}>{children}</button>
)
复制代码
如今咱们已经解决了全部的错误了!很是好!但咱们还能够作的更好!
在@types/react
中已经预约义一个类型type SFC<P>
,它也是类型interface StatelessComponent<P>
的一个别名,此外,它已经有预约义的children
和其余(defaultProps、displayName等等…),因此咱们不用每次都本身编写!
因此最后的无状态组件是这样的:
import React, { MouseEvent, SFC } from 'react';
type Props = { onClick(e: MouseEvent<HTMLElement>): void };
const Button: SFC<Props> = ({ onClick: handleClick, children }) => (
<button onClick={handleClick}>{children}</button>
);
复制代码
让咱们使用咱们的Button组件来建立有状态的计数器组件。
首先咱们须要定义initialState
const initialState = { clicksCount: 0 }
复制代码
如今咱们将使用TypeScript来从咱们的实现中推断出State的类型。
这样咱们不须要分开维护咱们的类型定义和实现,咱们只有惟一的真相源,即咱们的实现,太好了!
type State = Readonly<typeof initialState>
复制代码
另外请注意,该类型被明确映射为使全部的属性均为只读的。咱们须要再次使用State类型来显式地在咱们的class上定义只读的state属性。
readonly state: State = initialState
复制代码
这么作的做用是什么?
咱们知道咱们在React中不能像下面这样直接更新
state
:
this.state.clicksCount = 2;
this.state = { clicksCount: 2 }
复制代码
这将致使运行时错误,但在编译时不会报错。经过显式地使用
Readonly
映射咱们的type State
,和在咱们的类定义中设置只读的state属性,TS将会让咱们马上知道咱们作错了。
咱们的容器组件尚未任何Props API,因此咱们须要将Compoent
组件的第一个泛型参数定义为Object
(由于在React中props
永远是对象{}
),并使用State
类型做为第二个泛型参数。
import React, { Component } from 'react';
import Button from './Button';
const initialState = { clicksCount: 0 };
type State = Readonly<typeof initialState>;
class ButtonCounter extends Component<object, State> {
readonly state: State = initialState;
render() {
const { clicksCount } = this.state;
return (
<>
<Button onClick={this.handleIncrement}>Increment</Button>
<Button onClick={this.handleDecrement}>Decrement</Button>
You've clicked me {clicksCount} times!
</>
);
}
private handleIncrement = () => this.setState(incrementClicksCount);
private handleDecrement = () => this.setState(decrementClicksCount);
}
const incrementClicksCount = (prevState: State) => ({
clicksCount: prevState.clicksCount + 1,
});
const decrementClicksCount = (prevState: State) => ({
clicksCount: prevState.clicksCount - 1,
});
复制代码
你可能已经注意到了咱们将状态更新函数提取到类的外部做为纯函数。这是一种常见的模式,这样咱们不须要了解渲染逻辑就能够简单的测试这些状态更新函数。此外,由于咱们使用了TypeScript并将State显式地映射为只读的,它将阻止咱们在这些函数中作一些更改状态的操做:
const decrementClicksCount = (prevState: State) => ({
clicksCount: prevState.clicksCount--,
});
// 这样讲抛出编译错误:
//
// [ts] Cannot assign to 'clicksCount' because it is a constant or a read-only property.
复制代码
很是酷是吧?:)
让咱们扩展咱们的Button组件,新增一个string类型的颜色属性。
type Props = {
onClick(e: MouseEvent<HTMLElement>): void;
color: string;
};
复制代码
若是咱们想定义默认属性,咱们能够在咱们的组件中经过Button.defaultProps = {…}
来定义。
经过这样作,咱们须要改变咱们的属性类型定义来标记属性是可选有默认值的。
因此定义是这样的(注意?
操做符)
type Props = {
onClick(e: MouseEvent<HTMLElement>): void;
color?: string;
};
复制代码
此时咱们的组件实现是这样的:
const Button: SFC<Props> = ({ onClick: handleClick, color, children }) => (
<button style={{ color }} onClick={handleClick}>
{children}
</button>
);
复制代码
尽管这样在咱们简单的例子中可用的,这有一个问题。由于咱们在strict mode模式洗啊,可选的属性color
的类型是一个联合类型undefined | string
。
好比咱们想对color属性作一些操做,TS将会抛出一个错误,由于它并不知道它在React建立中经过Component.defaultProps
中已经定义了:
为了知足TS编译器,咱们可使用下面3种技术:
undefined
,尽管它是可选的,如:<button onClick={handleClick!}>{children}</button>
<button onClick={handleClick ? handleClick : undefined}>{children}</button>
withDefaultProps
__高阶函数,它将更新咱们的props类型定义和设置默认属性。我认为这是最简洁干净的方案。咱们能够很简单的实现咱们的高阶函数(感谢TS 2.8种的条件类型映射):
export const withDefaultProps = <
P extends object,
DP extends Partial<P> = Partial<P>
>(
defaultProps: DP,
Cmp: ComponentType<P>,
) => {
// 提取出必须的属性
type RequiredProps = Omit<P, keyof DP>;
// 从新建立咱们的属性定义,经过一个相交类型,将全部的原始属性标记成可选的,必选的属性标记成可选的
type Props = Partial<DP> & Required<RequiredProps>;
Cmp.defaultProps = defaultProps;
// 返回从新的定义的属性类型组件,经过将原始组件的类型检查关闭,而后再设置正确的属性类型
return (Cmp as ComponentType<any>) as ComponentType<Props>;
};
复制代码
如今咱们可使用withDefaultProps
高阶函数来定义咱们的默认属性,同时也解决了以前的问题:
const defaultProps = {
color: 'red',
};
type DefaultProps = typeof defaultProps;
type Props = { onClick(e: MouseEvent<HTMLElement>): void } & DefaultProps;
const Button: SFC<Props> = ({ onClick: handleClick, color, children }) => (
<button style={{ color }} onClick={handleClick}>
{children}
</button>
);
const ButtonWithDefaultProps = withDefaultProps(defaultProps, Button);
复制代码
或者直接使用内联(注意咱们须要显式的提供原始Button组件的属性定义,TS不能从函数中推断出参数的类型):
const ButtonWithDefaultProps = withDefaultProps<Props>(
defaultProps,
({ onClick: handleClick, color, children }) => (
<button style={{ color }} onClick={handleClick}>
{children}
</button>
),
);
复制代码
如今Button组件的属性已经被正确的定义被使用的,默认属性被反应出来而且在类型定义中是可选的,但在实现中是必选的!
{
onClick(e: MouseEvent<HTMLElement>): void
color?: string
}
复制代码
组件使用方法仍然是同样的:
render() {
return (
<ButtonWithDefaultProps
onClick={this.handleIncrement}
>
Increment
</ButtonWithDefaultProps>
)
}
复制代码
固然这也使用与经过class
定义的组件(得益于TS中的类结构起源,咱们不须要显式指定咱们的Props
泛型类型)。
它看起来像这样:
const ButtonViaClass = withDefaultProps(
defaultProps,
class Button extends Component<Props> {
render() {
const { onClick: handleClick, color, children } = this.props;
return (
<button style={{ color }} onClick={handleClick}>
{Children}
</button>
);
}
},
);
复制代码
再次,它的使用方式仍然是同样的:
render() {
return (
<ButtonViaClass onClick={this.handleIncrement}>Increment</ButtonViaClass>
);
}
复制代码
好比说你须要构建一个可展开的菜单组件,它须要在用户点击它时显示子内容。咱们就可使用各类各样的组件模式来实现它。
实现组件的逻辑可复用的最好方式将组件的children放到函数中去,或者利用render
属性API——这也是为何Render回调也被称为函数子组件。
让咱们用render属性方法实现一个Toggleable
组件:
import React, { Component, MouseEvent } from 'react';
import { isFunction } from '../utils';
const initialState = {
show: false,
};
type State = Readonly<typeof initialState>;
type Props = Partial<{
children: RenderCallback;
render: RenderCallback;
}>;
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element;
type ToggleableComponentProps = {
show: State['show'];
toggle: Toggleable['toggle'];
};
export class Toggleable extends Component<Props, State> {
readonly state: State = initialState;
render() {
const { render, children } = this.props;
const renderProps = {
show: this.state.show,
toggle: this.toggle,
};
if (render) {
return render(renderProps);
}
return isFunction(children) ? children(renderProps) : null;
}
private toggle = (event: MouseEvent<HTMLElement>) =>
this.setState(updateShowState);
}
const updateShowState = (prevState: State) => ({ show: !prevState.show });
复制代码
这里都发生了什么,让咱们来分别看看重要的部分:
const initialState = {
show: false,
};
type State = Readonly<typeof initialState>;
复制代码
如今咱们来定义组件的props(注意这里咱们使用了Partitial映射类型,由于咱们全部的属性都是可选的,不用分别对每一个属性手动添加?
标识符):
type Props = Partial<{
children: RenderCallback;
render: RenderCallback;
}>;
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element;
type ToggleableComponentProps = {
show: State['show'];
toggle: Toggleable['toggle'];
};
复制代码
咱们须要同时支持child做为函数,和render属性做为函数,它们二者都是可选的。为了不重复代码,咱们定义了RenderCallback
做为咱们的渲染函数定义:
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element
复制代码
在读者眼中看起来比较奇怪的部分是咱们最后的别名类型:
type ToggleableComponentProps
!
type ToggleableComponentProps = {
show: State['show'];
toggle: Toggleable['toggle'];
};
复制代码
这里咱们使用了TypeScript的__查找类型(lookup types)__,因此咱们又不须要重复地去定义类型了:
show: State['show']
咱们利用已有的state类型定义了show
属性toggle: Toggleable['toggle']
咱们利用了TS从类实现推断类类型来定义toggle
属性。很好用并且很是强大。剩下的实现部分很简单,标准的render属性/children做为函数的模式:
export class Toggleable extends Component<Props, State> {
// ...
render() {
const { render, children } = this.props;
const renderProps = {
show: this.state.show,
toggle: this.toggle,
};
if (render) {
return render(renderProps);
}
return isFunction(children) ? children(renderProps) : null;
}
// ...
}
复制代码
如今咱们能够把函数做为children传给Toggleable组件了:
<Toggleable>
{({ show, toggle }) => (
<>
<div onClick={toggle}>
<h1>some title</h1>
</div>
{show ? <p>some content</p> : null}
</>
)}
</Toggleable>
复制代码
或者咱们能够把函数做为render属性:
<Toggleable
render={({ show, toggle }) => (
<>
<div onClick={toggle}>
<h1>some title</h1>
</div>
{show ? <p>some content</p> : null}
</>
)}
/>
复制代码
感谢TypeScript,咱们在render属性的参数有了智能提示和正确的类型检查:
若是咱们想复用它(好比用在多个菜单组件中),咱们只须要建立一个使用Toggleable逻辑的心组件:
type Props = { title: string }
const ToggleableMenu: SFC<Props> = ({ title, children }) => (
<Toggleable
render={({ show, toggle }) => (
<>
<div onClick={toggle}>
<h1>title</h1>
</div>
{show ? children : null}
</>
)}
/>
)
复制代码
如今咱们全新的__ToggleableMenu
__组件已经能够在Menu组件中使用了:
export class Menu extends Component {
render() {
return (
<>
<ToggleableMenu title="First Menu">Some content</ToggleableMenu>
<ToggleableMenu title="Second Menu">Some content</ToggleableMenu>
<ToggleableMenu title="Third Menu">Some content</ToggleableMenu>
</>
);
}
}
复制代码
而且它也像咱们指望的那样工做了:
这中模式在咱们想更改渲染的内容,而不关心状态改变的状况下很是有用:能够看到,咱们将渲染逻辑移到ToggleableMenu组件的额children函数中了,但把状态管理逻辑保留在咱们的Toggleable组件中!
为了让咱们的组件更灵活,咱们能够引入组件注入模式。
什么是组件注入模式呢?若是你对React-Router比较熟悉,那你已经在下面这样路由定义的时候使用这种模式了:
<Route path="/foo" component={MyView} />
复制代码
这样咱们不是把函数传递给render/children属性,而是经过component
属性“注入”组件。为此,咱们能够重构,把咱们的内置render属性函数改为一个可复用的无状态组件:
type MenuItemProps = { title: string };
const MenuItem: SFC<MenuItemProps & ToggleableComponentProps> = ({
title,
toggle,
show,
children,
}) => (
<>
<div onClick={toggle}>
<h1>{title}</h1>
</div>
{show ? children : null}
</>
);
复制代码
有了这个,咱们可使用render属性重构ToggleableMenu
:
type Props = { title: string };
const ToggleableMenu: SFC<Props> = ({ title, children }) => (
<Toggleable
render={({ show, toggle }) => (
<MenuItem show={show} toggle={toggle} title={title}>
{children}
</MenuItem>
)}
/>
);
复制代码
这个完成以后,让咱们来开始定义咱们新的API——compoent
属性。
咱们须要更新咱们的属性API。
children
如今能够是函数或者ReactNode(当component属性被使用时)component
是咱们新的API,它能够接受实现了ToggleableComponentProps
属性的组件,而且它须要是设置为any的泛型,这样各类各样的实现组件能够添加其余属性到ToggleableComponentProps
并经过TS的验证props
咱们引入能够传入任意属性的定义。它被定义成any类型的可索引类型,这里咱们放松了严格的类型安全检查...// 咱们须要使用咱们任意的props类型来建立 defaultProps,默认是一个空对象
const defaultProps = { props: {} as { [name: string]: any } };
type Props = Partial<
{
children: RenderCallback | ReactNode;
render: RenderCallback;
component: ComponentType<ToggleableComponentProps<any>>;
} & DefaultProps
>;
type DefaultProps = typeof defaultProps;
复制代码
下一步,咱们须要添加新的属性API到ToggleableComponentProps
上,以便用户能够经过<Toggleable props={...} />
来使用咱们的props
属性:
export type ToggleableComponentProps<P extends object = object> = {
show: State['show'];
toggle: Toggleable['toggle'];
} & P;
复制代码
而后咱们须要更新咱们的render
函数:
render() {
const {
component: InjectedComponent,
props,
render,
children,
} = this.props;
const renderProps = {
show: this.state.show,
toggle: this.toggle,
};
// 当 component 属性被使用时,children 是 ReactNode 而不是函数
if (InjectedComponent) {
return (
<InjectedComponent {...props} {...renderProps}>
{children}
</InjectedComponent>
);
}
if (render) {
return render(renderProps);
}
return isFunction(children) ? children(renderProps) : null;
}
复制代码
完整的Toggleable组件实现以下,支持 render 属性、children做为函数、组件注入功能:
import React, { Component, ReactNode, ComponentType, MouseEvent } from 'react';
import { isFunction, getHocComponentName } from '../utils';
const initialState = { show: false };
const defaultProps = { props: {} as { [name: string]: any } };
type State = Readonly<typeof initialState>;
type Props = Partial<
{
children: RenderCallback | ReactNode;
render: RenderCallback;
component: ComponentType<ToggleableComponentProps<any>>;
} & DefaultProps
>;
type DefaultProps = typeof defaultProps;
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element;
export type ToggleableComponentProps<P extends object = object> = {
show: State['show'];
toggle: Toggleable['toggle'];
} & P;
export class Toggleable extends Component<Props, State> {
static readonly defaultProps: Props = defaultProps;
readonly state: State = initialState;
render() {
const {
component: InjectedComponent,
props,
render,
children,
} = this.props;
const renderProps = {
show: this.state.show,
toggle: this.toggle,
};
if (InjectedComponent) {
return (
<InjectedComponent {...props} {...renderProps}>
{children}
</InjectedComponent>
);
}
if (render) {
return render(renderProps);
}
return isFunction(children) ? children(renderProps) : null;
}
private toggle = (event: MouseEvent<HTMLElement>) =>
this.setState(updateShowState);
}
const updateShowState = (prevState: State) => ({ show: !prevState.show });
复制代码
咱们最终使用component
属性的ToggleableMenuViaComponentInjection
组件是这样的:
const ToggleableMenuViaComponentInjection: SFC<ToggleableMenuProps> = ({
title,
children,
}) => (
<Toggleable component={MenuItem} props={{ title }}>
{children}
</Toggleable>
);
复制代码
请注意,这里咱们的props
属性没有严格的类型安全检查,由于它被定义成索引对象类型{ [name: string]: any }
:
咱们能够仍是像以前同样使用ToggleableMenuViaComponentInjection
组件来实现菜单渲染:
export class Menu extends Component {
render() {
return (
<>
<ToggleableMenuViaComponentInjection title="First Menu">
Some content
</ToggleableMenuViaComponentInjection>
<ToggleableMenuViaComponentInjection title="Second Menu">
Another content
</ToggleableMenuViaComponentInjection>
<ToggleableMenuViaComponentInjection title="Third Menu">
More content
</ToggleableMenuViaComponentInjection>
</>
);
}
}
复制代码
在咱们视线“组件注入模式”的时候,咱们失去了对props
属性严格的类型安全检查。咱们怎样修复这个问题呢?对,你猜到了!咱们能够把咱们的Toggleable
组件实现为一个泛型组件!
首先咱们须要把咱们的属性泛型化。咱们使用默认的泛型参数,因此咱们不须要在不必的时候显式地提供类型(针对 render 属性和 children 做为函数)。
type Props<P extends object = object> = Partial<
{
children: RenderCallback | ReactNode;
render: RenderCallback;
component: ComponentType<ToggleableComponentProps<P>>;
} & DefaultProps<P>
>;
复制代码
咱们也须要把ToggleableComponnetProps
更新成泛型的。不,等等,它已是泛型啦!因此还不须要作任何更改。
须要更新的是type DefaultProps
,由于不支持从声明实现推倒出泛型类型定义,因此须要把它重构成传统的类型定义->实现:
type DefaultProps<P extends object = object> = { props: P };
const defaultProps: DefaultProps = { props: {} };
复制代码
就快好啦!
如今让咱们把组件类也泛型化。再次说明,咱们使用了默认的属性,因此在没有使用组件注入的时候不须要去指定泛型参数!
export class Toggleable<T = {}> extends Component<Props<T>, State> {}
复制代码
这样就完成了吗?嗯…,咱们能够在JSX中使用泛型类型吗?
坏消息是,不能...
但咱们能够在泛型组件上引入ofType
的工场模式:
export class Toggleable<T = {}> extends Component<Props<T>, State> {
static ofType<T extends object>() {
return Toggleable as Constructor<Toggleable<T>>;
}
}
复制代码
完整的 Toggleable 组件实现,支持 Render 属性、Children 做为函数、带泛型 props 属性支持的组件注入:
import React, {
Component,
ReactNode,
ComponentType,
MouseEvent,
SFC,
} from 'react';
import { isFunction, getHocComponentName } from '../utils';
const initialState = { show: false };
// const defaultProps = { props: {} as { [name: string]: any } };
type State = Readonly<typeof initialState>;
type Props<P extends object = object> = Partial<
{
children: RenderCallback | ReactNode;
render: RenderCallback;
component: ComponentType<ToggleableComponentProps<P>>;
} & DefaultProps<P>
>;
type DefaultProps<P extends object = object> = { props: P };
const defaultProps: DefaultProps = { props: {} };
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element;
export type ToggleableComponentProps<P extends object = object> = {
show: State['show'];
toggle: Toggleable['toggle'];
} & P;
export class Toggleable<T = {}> extends Component<Props<T>, State> {
static ofType<T extends object>() {
return Toggleable as Constructor<Toggleable<T>>;
}
static readonly defaultProps: Props = defaultProps;
readonly state: State = initialState;
render() {
const {
component: InjectedComponent,
props,
render,
children,
} = this.props;
const renderProps = {
show: this.state.show,
toggle: this.toggle,
};
if (InjectedComponent) {
return (
<InjectedComponent {...props} {...renderProps}>
{children}
</InjectedComponent>
);
}
if (render) {
return render(renderProps);
}
return isFunction(children) ? children(renderProps) : null;
}
private toggle = (event: MouseEvent<HTMLElement>) =>
this.setState(updateShowState);
}
const updateShowState = (prevState: State) => ({ show: !prevState.show });
复制代码
有了static ofType
工厂函数后,咱们能够建立正确类型的泛型组件了。
type MenuItemProps = { title: string };
// ofType 是一种标识函数,返回的是相同实现的 Toggleable 组件,但带有制定的 props 类型
const ToggleableWithTitle = Toggleable.ofType<MenuItemProps>();
type ToggleableMenuProps = MenuItemProps;
const ToggleableMenuViaComponentInjection: SFC<ToggleableMenuProps> = ({
title,
children,
}) => (
<ToggleableWithTitle component={MenuItem} props={{ title }}>
{children}
</ToggleableWithTitle>
);
复制代码
而且全部的东西都还像一块儿同样工做,但此次我有的 props={}
属性有了正确的类型检查。鼓掌吧!
由于咱们已经建立了带render回调功能的Toggleable
组件,实现HOC也会很容易。(这也是 render 回调函数模式的一个大优点,由于咱们可使用HOC来实现)
让咱们开始实现咱们的HOC组件吧:
咱们须要建立:
hoist-non-react-statics
npm包中的hoistNonReactStatics
import React, { ComponentType, Component } from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import { getHocComponentName } from '../utils';
import {
Toggleable,
Props as ToggleableProps,
ToggleableComponentProps,
} from './RenderProps';
// OwnProps 是内部组件上任意公开的属性
type OwnProps = object;
type InjectedProps = ToggleableComponentProps;
export const withToggleable = <OriginalProps extends object>(
UnwrappedComponent: ComponentType<OriginalProps & InjectedProps>,
) => {
// 咱们使用 TS 2.8 中的条件映射类型来获得咱们最终的属性类型
type Props = Omit<OriginalProps, keyof InjectedProps> & OwnProps;
class WithToggleable extends Component<Props> {
static readonly displayName = getHocComponentName(
WithToggleable.displayName,
UnwrappedComponent,
);
static readonly UnwrappedComponent = UnwrappedComponent;
render() {
const { ...rest } = this.props;
return (
<Toggleable
render={renderProps => (
<UnwrappedComponent {...rest} {...renderProps} />
)}
/>
);
}
}
return hoistNonReactStatics(WithToggleable, UnwrappedComponent);
};
复制代码
如今咱们可使用HOC来建立咱们的Toggleable
菜单组件了,并有正确的类型安全检查!
const ToggleableMenuViaHOC = withToggleable(MenuItem)
复制代码
一切正常,还有类型安全检查!好极了!
这是最后一个组件模式了!假设咱们想从父组件中控制咱们的Toggleable
组件,咱们须要Toggleable
组件配置化。这是一种很强大的模式。让咱们来实现它吧。
当我说受控组件时,我指的是什么?我想从Menu
组件内控制因此的ToggleableManu
组件的内容是否显示。
咱们须要像这样更新咱们的ToggleableMenu
组件的实现:
// 更新咱们的属性类型,以便咱们能够经过 show 属性来控制是否显示
type Props = MenuItemProps & { show?: boolean };
// 注意:这里咱们使用告终构来建立变量别,为了避免和 render 回调函数的 show 参数冲突
// -> { show: showContent }
// Render 属性
export const ToggleMenu: SFC<ToggleableComponentProps> = ({
title,
children,
show: showContent,
}) => (
<Toggleable show={showContent}>
{({ show, toggle }) => (
<MenuItem title={title} toggle={toggle} show={show}>
{children}
</MenuItem>
)}
</Toggleable>
);
// 组件注入
const ToggleableWithTitle = Toggleable.ofType<MenuItemProps>();
export const ToggleableMenuViaComponentInjection: SFC<Props> = ({
title,
children,
show: showContent,
}) => (
<ToggleableWithTitle
component={MenuItem}
props={{ title }}
show={showContent}
>
{children}
</ToggleableWithTitle>
);
// HOC不须要更改
export const ToggleMenuViaHOC = withToggleable(MenuItem);
复制代码
有了这些更新后,咱们能够在Menu
中添加状态,并传递给ToggleableMenu
const initialState = { showContents: false };
type State = Readonly<typeof initialState>;
export class Menu extends Component<object, State> {
readonly state: State = initialState;
render() {
const { showContents } = this.state;
return (
<>
<button onClick={this.toggleShowContents}>toggle showContent</button>
<hr />
<ToggleableMenu title="First Menu" show={showContents}>
Some Content
</ToggleableMenu>
<ToggleableMenu title="Second Menu" show={showContents}>
Another Content
</ToggleableMenu>
<ToggleableMenu title="Third Menu" show={showContents}>
More Content
</ToggleableMenu>
</>
);
}
}
复制代码
让咱们为了最终的功能和灵活性最后一次更新Toggleable
组件。为了让 Toggleable 变成受控组件咱们须要:
show
属性到Props
API上1 & 2:
const initialState = { show: false }
const defaultProps: DefaultProps = { ...initialState, props: {} }
type State = Readonly<typeof initialState>
type DefaultProps<P extends object = object> = { props: P } & Pick<State, 'show'>
复制代码
3 & 4:
export class Toggleable<T = {}> extends Component<Props<T>, State> {
static readonly defaultProps: Props = defaultProps
// Bang operator used, I know I know ...
state: State = { show: this.props.show! }
componentWillReceiveProps(nextProps: Props<T>) {
const currentProps = this.props
if (nextProps.show !== currentProps.show) {
this.setState({ show: Boolean(nextProps.show) })
}
}
}
复制代码
最终支持全部全部模式(Render属性/Children做为函数/组件注入/泛型组件/受控组件)的 Toggleable 组件:
import React, { Component, MouseEvent, ComponentType, ReactNode } from 'react'
import { isFunction, getHocComponentName } from '../utils'
const initialState = { show: false }
const defaultProps: DefaultProps = { ...initialState, props: {} }
type State = Readonly<typeof initialState>
export type Props<P extends object = object> = Partial<
{
children: RenderCallback | ReactNode
render: RenderCallback
component: ComponentType<ToggleableComponentProps<P>>
} & DefaultProps<P>
>
type RenderCallback = (args: ToggleableComponentProps) => JSX.Element
export type ToggleableComponentProps<P extends object = object> = {
show: State['show']
toggle: Toggleable['toggle']
} & P
type DefaultProps<P extends object = object> = { props: P } & Pick<State, 'show'>
export class Toggleable<T extends object = object> extends Component<Props<T>, State> {
static ofType<T extends object>() {
return Toggleable as Constructor<Toggleable<T>>
}
static readonly defaultProps: Props = defaultProps
readonly state: State = { show: this.props.show! }
componentWillReceiveProps(nextProps: Props<T>, nextContext: any) {
const currentProps = this.props
if (nextProps.show !== currentProps.show) {
this.setState({ show: Boolean(nextProps.show) })
}
}
render() {
const { component: InjectedComponent, children, render, props } = this.props
const renderProps = { show: this.state.show, toggle: this.toggle }
if (InjectedComponent) {
return (
<InjectedComponent {...props} {...renderProps}>
{children}
</InjectedComponent>
)
}
if (render) {
return render(renderProps)
}
return isFunction(children) ? children(renderProps) : new Error('asdsa()')
}
private toggle = (event: MouseEvent<HTMLElement>) => this.setState(updateShowState)
}
const updateShowState = (prevState: State) => ({ show: !prevState.show })
复制代码
最终的Toggleable HOC 组件 withToggleable
只须要稍做修改 -> 咱们须要在HOC组件中传递 show
属性,并更新咱们的OwnProps
API
import React, { ComponentType, Component } from 'react'
import hoistNonReactStatics from 'hoist-non-react-statics'
import { getHocComponentName } from '../utils'
import {
Toggleable,
Props as ToggleableProps,
ToggleableComponentProps as InjectedProps,
} from './toggleable'
// OwnProps is for any public props that should be available on internal Component.props
// and for WrappedComponent
type OwnProps = Pick<ToggleableProps, 'show'>
export const withToogleable = <OriginalProps extends object>(
UnwrappedComponent: ComponentType<OriginalProps & InjectedProps>
) => {
// we are leveraging TS 2.8 conditional mapped types to get proper final prop types
type Props = Omit<OriginalProps, keyof InjectedProps> & OwnProps
class WithToggleable extends Component<Props> {
static readonly displayName = getHocComponentName(
WithToggleable.displayName,
UnwrappedComponent
)
static readonly WrappedComponent = UnwrappedComponent
render() {
// Generics and spread issue
// https://github.com/Microsoft/TypeScript/issues/10727
const { show, ...rest } = this.props as Pick<Props, 'show'> // we need to explicitly pick props we wanna destructure, rest is gonna be type `{}`
return (
<Toggleable
show={show}
render={renderProps => <UnwrappedComponent {...rest} {...renderProps} />}
/>
)
}
}
return hoistNonReactStatics(WithToggleable, UnwrappedComponent as any) as ComponentType<Props>
}
复制代码
使用 TypeScript 和 React 时,实现恰当的类型安全组件可能会很棘手。但随着 TypeScript 2.8中新加入的功能,咱们几乎能够在全部的 React 组件模式中编写类型安全的组件。
在这遍很是长(对此十分抱歉)文章中,感谢TypeScript,咱们已经学会了在各类各样的模式下怎么编写严格类型安全检查的组件。
在这些模式中最强的应该是Render属性模式,它让咱们能够在此基础上不须要太多改动就能够实现其余常见的模式,如组件注入、高阶组件等。
文中全部的demo均可以在个人 Github 仓库中找到。
此外,须要明白的是,本文中演示的模版类型安全,只能在使用 VDOM/JSX 的库中实现。
和往常同样,若是你有任何问题,能够在这或者 twitter(@martin_hotell)联系我,另外,快乐的类型检查伙伴们,干杯!
对咱们团队感兴趣的能够关注专栏,关注github或者发送简历至'tao.qit####alibaba-inc.com'.replace('####', '@'),欢迎有志之士加入~