): void;
42 | children?: ReactNode;
43 | };
44 |
45 | const Button = ({ onClick: handleClick, children }: Props) => (
46 |
47 | );
48 | ```
49 |
50 |
51 |
52 | OK,错误没有了!好像已经完事了?其实再花点心思可以做的更好。
53 |
54 | React 中有个预定义的类型,`SFC` :
55 |
56 | ```typescript
57 | type SFC = StatelessComponent
;
58 | ```
59 |
60 | 他是 `StatelessComponent` 的一个别名,而 `StatelessComponent` 声明了纯函数组件的一些预定义示例属性和静态属性,如:`children`、`defaultProps`、`displayName` 等,所以我们不需要自己写所有的东西!
61 |
62 |
63 |
64 | 最后我们的代码是这样的:
65 |
66 | 
67 |
68 |
69 |
70 |
71 |
72 | ## 有状态的类组件
73 |
74 | 接着我们来创建一个计数器按钮组件。首先我们定义初始状态:
75 |
76 | ```typescript
77 | const initialState = {count: 0};
78 | ```
79 |
80 |
81 |
82 | 然后,定义一个别名 `State` 并用 TS 推断出类型:
83 |
84 | ```typescript
85 | type State = Readonly;
86 | ```
87 |
88 | > 知识点:这样做不用分开维护接口声明和实现代码,比较实用的技巧
89 |
90 |
91 |
92 | 同时应该注意到,我们将所有的状态属性声明为 `readonly` 。然后我们需要明确定义 state 为组件的实例属性:
93 |
94 | ```typescript
95 | readonly state: State = initialState;
96 | ```
97 |
98 | 为什么要这样做?我们知道在 React 中我们不能直接改变 `State` 的属性值或者 `State` 本身:
99 |
100 | ```typescript
101 | this.state.count = 1;
102 | this.state = {count: 1};
103 | ```
104 |
105 | 如果这样做在运行时将会抛出错误,但在编写代码时却不会。所以我们需要明确的声明 `readonly` ,这样 TS 会让我们知道如果执行了这种操作就会出错了:
106 |
107 | 
108 |
109 |
110 |
111 | 下面是完整的代码:
112 |
113 | > 这个组件不需要外部传递任何 `Props` ,所以泛型的第一个参数给的是不带任何属性的对象
114 |
115 | 
116 |
117 |
118 |
119 |
120 |
121 | ## 属性默认值
122 |
123 | 让我们来扩展一下纯函数按钮组件,加上一个颜色属性:
124 |
125 | ```typescript
126 | interface Props {
127 | onClick(e: MouseEvent): void;
128 | color: string;
129 | }
130 | ```
131 |
132 | 如果想要定义属性默认值的话,我们知道可以通过 `Button.defaultProps = {...}` 做到。并且我们需要把这个属性声明为可选属性:(注意属性名后的 `?` )
133 |
134 | ```typescript
135 | interface Props {
136 | onClick(e: MouseEvent): void;
137 | color?: string;
138 | }
139 | ```
140 |
141 |
142 |
143 | 那么组件现在看起来是这样的:
144 |
145 | ```typescript
146 | const Button: SFC = ({onClick: handleClick, color, children}) => (
147 |
148 | );
149 | ```
150 |
151 | 一切看起来好像都很简单,但是这里有一个“痛点”。注意我们使用了 TS 的严格模式,`color?: string` 这个可选属性的类型现在是联合类型 -- `string | undefined` 。
152 |
153 | 这意味着什么?如果你要对这种属性进行一些操作,比如:`substr()` ,TS 编译器会直接报错,因为类型有可能是 `undefined` ,TS 并不知道属性默认值会由 `Component.defaultProps` 来创建。
154 |
155 |
156 |
157 | 碰到这种情况我们一般用两种方式来解决:
158 |
159 | - 使用类型断言手动去除,添加 `!` 后缀,像这样:`color!.substr(...)` 。
160 | - 使用条件判断或者三元操作符让 TS 编译器知道这个属性不是 undefined,比如: `if (color) ...` 。
161 |
162 |
163 |
164 | 以上的方式虽然可以工作但有种多此一举的感觉,毕竟默认值已经有了只是 TS 编译器“不知道”而已。下面来说一种可重用的方案:我们写一个 `withDefaultProps` 函数,利用 TS 2.8 的条件类型映射,可以很简单的完成:
165 |
166 | 
167 |
168 | 这里涉及到两个 type 定义,写在 `src/types/global.d.ts` 文件里面:
169 |
170 | ```typescript
171 | declare type DiffPropertyNames =
172 | { [P in T]: P extends U ? never: P }[T];
173 |
174 | declare type Omit = Pick>;
175 | ```
176 |
177 | 看一下 [TS 2.8 的新特性说明](http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html) 关于 `Conditional Types` 的说明,就知道这两个 `type` 的原理了。
178 |
179 | > 注意 TS 2.9 的新变化:`keyof T` 的类型是 `string | number | symbol ` 的结构子类型。
180 |
181 |
182 |
183 | 现在我们可以利用 `withDefaultProps` 函数来写一个有属性默认值的组件了:
184 |
185 | 
186 |
187 | 现在使用这个组件时默认值属性已经发生作用,是可选的;并且在组件内部使用这些默认值属性不用再手动断言了,这些默认值属性就是必填属性!感觉还不错对吧 :smile:
188 |
189 | > `withDefautProps` 函数同样可以应用在 `stateful` 有状态的类组件上。
190 |
191 |
192 |
193 |
194 |
195 | ## 渲染回调模式
196 |
197 | 有一种重用组件逻辑的设计方式是:把组件的 `children` 写成渲染回调函数或者暴露一个 `render` 函数属性出来。我们将用这种思路来做一个折叠面板的场景应用。
198 |
199 | 首先我们先写一个 `Toggleable` 组件,完整的代码如下:
200 |
201 | 
202 |
203 |
204 |
205 | 下面我们来逐段解释下这段代码,首先先看到组件的属性声明相关部分:
206 |
207 | ```typescript
208 | type Props = Partial<{
209 | children: RenderCallback;
210 | render: RenderCallback;
211 | }>;
212 |
213 | type RenderCallback = (args: ToggleableRenderArgs) => React.ReactNode;
214 |
215 | type ToggleableRenderArgs = {
216 | show: boolean;
217 | toggle: Toggleable['toggle'];
218 | }
219 | ```
220 |
221 | 我们需要同时支持 `children` 或 `render` 函数属性,所以这两个要声明为可选的属性。注意这里用了 `Partial` 映射类型,这样就不需要每个手动 `?` 操作符来声明可选了。
222 |
223 | 为了保持 ***DRY*** 原则(Don't repeat yourself ),我们还声明了 `RenderCallback` 类型。
224 |
225 | 最后,我们将这个回调函数的参数声明为一个独立的类型:`ToggleableRenderArgs` 。
226 |
227 | 注意我们使用了 TS 的**查找类型**(*lookup types* ),这样 `toggle` 的类型将和类中定义的同名方法类型保持一致:
228 |
229 | ```typescript
230 | private toggle = (event: MouseEvent) => {
231 | this.setState(prevState => ({show: !prevState.show}));
232 | };
233 | ```
234 |
235 | > 同样是为了 DRY ,TS 非常给力!
236 |
237 |
238 |
239 | 接下来是 State 相关的:
240 |
241 | ```typescript
242 | const initialState = {show: false};
243 | type State = Readonly;
244 | ```
245 |
246 | 这个没什么特别的,跟前面的例子一样。
247 |
248 |
249 |
250 | 剩下的部分就是 渲染回调 设计模式了,代码很好理解:
251 |
252 | ```typescript
253 | class Toggleable extends Component {
254 |
255 | // ...
256 |
257 | render() {
258 | const {children, render} = this.props;
259 | const {show} = this.state;
260 | const renderArgs = {show, toggle: this.toggle};
261 |
262 | if (render) {
263 | return render(renderArgs);
264 | } else if (isFunction(children)) {
265 | return children(renderArgs);
266 | } else {
267 | return null;
268 | }
269 | }
270 |
271 | // ...
272 | }
273 | ```
274 |
275 |
276 |
277 | 现在我们可以将 children 作为一个渲染函数传递给 Toggleable 组件:
278 |
279 | 
280 |
281 | 或者将渲染函数传递给 render 属性:
282 |
283 | 
284 |
285 |
286 |
287 | 下面我们来完成折叠面板剩下的工作,先写一个 Panel 组件来重用 Toggleable 的逻辑:
288 |
289 | 
290 |
291 | 最后写一个 Collapse 组件来完成这个应用:
292 |
293 | 
294 |
295 |
296 |
297 | 这里我们不谈样式的事情,运行起来看看,跟期待的效果是否一致?
298 |
299 | 
300 |
301 | > 这种方式对于需要扩展渲染内容时非常有用:Toggleable 组件并不知道也不关心具体的渲染内容,但他控制着显示状态逻辑!
302 |
303 |
304 |
305 |
306 |
307 | ## 组件注入模式
308 |
309 | 为了使组件逻辑更具伸缩性,下面我们来说说组件注入模式。
310 |
311 |
312 |
313 | 那么什么是组件注入模式呢?如果你用过 `React-Router` ,你已经使用过这种模式来定义路由了:
314 |
315 | ```jsx
316 |
317 | ```
318 |
319 |
320 |
321 | 不同于渲染回调模式,我们使用 `component` 属性“注入”一个组件。为了演示这个模式是如何工作的,我们将重构折叠面板这个场景,首先写一个可重用的 PanelItem 组件:
322 |
323 | ```typescript
324 | import { ToggleableComponentProps } from './Toggleable';
325 |
326 | type PanelItemProps = { title: string };
327 |
328 | const PanelItem: SFC = props => {
329 | const {title, children, show, toggle} = props;
330 |
331 | return (
332 |
333 |
{title}
334 | {show ? children : null}
335 |
336 | );
337 | };
338 | ```
339 |
340 |
341 |
342 | 然后重构 Toggleable 组件:加入新的 `component` 属性。对比先头的代码,我们需要做出如下变化:
343 |
344 | - `children` 属性类型更改为 function 或者 ReactNode(当使用 `component` 属性时)
345 | - `component` 属性将传递一个组件注入进去,这个注入组件的属性定义上需要有 `ToggleableComponentProps` (其实是原来的 `ToggleableRenderArgs` ,还记得吗?)
346 | - 还需要定义一个 ` props` 属性,这个属性将用来传递注入组件需要的属性值。我们会设置 `props` 可以拥有任意的属性,因为我们并不知道注入组件会有哪些属性,当然这样我们会丢失 TS 的严格类型检查...
347 |
348 | ```typescript
349 | const defaultInjectedProps = {props: {} as { [propName: string]: any }};
350 | type DefaultInjectedProps = typeof defaultInjectedProps;
351 | type Props = Partial<{
352 | children: RenderCallback | ReactNode;
353 | render: RenderCallback;
354 | component: ComponentType>
355 | } & DefaultInjectedProps>;
356 | ```
357 |
358 |
359 |
360 | 下一步我们把原来的 `ToggleableRenderArgs` 修改为 `ToggleableComponentProps` ,允许将注入组件需要的属性通过 ` ` 这样来传递:
361 |
362 | ```typescript
363 | type ToggleableComponentProps = {
364 | show: boolean;
365 | toggle: Toggleable['toggle'];
366 | } & P;
367 | ```
368 |
369 |
370 |
371 | 现在我们还需要重构一下 `render` 方法:
372 |
373 | ```typescript
374 | render() {
375 | const {component: InjectedComponent, children, render, props} = this.props;
376 | const {show} = this.state;
377 | const renderProps = {show, toggle: this.toggle};
378 |
379 | if (InjectedComponent) {
380 | return (
381 |
382 | {children}
383 |
384 | );
385 | }
386 |
387 | if (render) {
388 | return render(renderProps);
389 | } else if (isFunction(children)) {
390 | return children(renderProps);
391 | } else {
392 | return null;
393 | }
394 | }
395 | ```
396 |
397 |
398 |
399 | 我们已经完成了整个 Toggleable 组件的修改,下面是完整的代码:
400 |
401 | 
402 |
403 |
404 |
405 | 最后我们写一个 `PanelViaInjection` 组件来应用组件注入模式:
406 |
407 | ```typescript
408 | import React, { SFC } from 'react';
409 | import { Toggleable } from './Toggleable';
410 | import { PanelItemProps, PanelItem } from './PanelItem';
411 |
412 | const PanelViaInjection: SFC = ({title, children}) => (
413 |
414 | {children}
415 |
416 | );
417 | ```
418 |
419 | > 注意:`props` 属性没有类型安全检查,因为他被定义为了包含任意属性的可索引类型:
420 | > `{ [propName: string]: any }`
421 |
422 |
423 |
424 | 现在我们可以利用这种方式来重现折叠面板场景了:
425 |
426 | ```typescript
427 | class Collapse extends Component {
428 |
429 | render() {
430 | return (
431 |
432 |
内容1
433 |
内容2
434 |
内容3
435 |
436 | );
437 | }
438 | }
439 | ```
440 |
441 |
442 |
443 |
444 |
445 | ## 泛型组件
446 |
447 | 在组件注入模式的例子中,`props` 属性丢失了类型安全检查,我们如何去修复这个问题呢?估计你已经猜出来了,我们可以把 Toggleable 组件重构为泛型组件!
448 |
449 |
450 |
451 | 下来我们开始重构 Toggleable 组件。首先我们需要让 `props` 支持泛型:
452 |
453 | ```typescript
454 | type DefaultInjectedProps = { props: P };
455 | const defaultInjectedProps: DefaultInjectedProps = {props: {}};
456 |
457 | type Props
= Partial<{
458 | children: RenderCallback | ReactNode;
459 | render: RenderCallback;
460 | component: ComponentType>
461 | } & DefaultInjectedProps>;
462 | ```
463 |
464 |
465 |
466 | 然后让 Toggleable 的 class 也支持泛型:
467 |
468 | ```typescript
469 | class Toggleable extends Component, State> {}
470 | ```
471 |
472 | 看起来好像已经搞定了!如果你是用的 TS 2.9,可以直接这样用:
473 |
474 | ```typescript
475 | const PanelViaInjection: SFC = ({title, children}) => (
476 | component={PanelItem} props={{title}}>
477 | {children}
478 |
479 | );
480 | ```
481 |
482 |
483 |
484 | 但是如果 <= TS 2.8 ... JSX 里面不能直接应用泛型参数 :worried: 那么我们还有一步工作要做,加入一个静态方法 `ofType` ,用来进行构造函数的类型转换:
485 |
486 | ```typescript
487 | static ofType() {
488 | return Toggleable as Constructor>;
489 | }
490 | ```
491 |
492 | 这里用到一个 type:`Constructor`,依然定义在 `src/types/global.d.ts` 里面:
493 |
494 | ```typescript
495 | declare type Constructor = { new(...args: any[]): T };
496 | ```
497 |
498 |
499 |
500 | 好了,我们完成了所有的工作,下面是 Toggleable 重构后的完整代码:
501 |
502 |
503 |
504 | 现在我们来看看怎么使用这个泛型组件,重构下原来的 PanelViaInjection 组件:
505 |
506 | ```typescript
507 | import React, { SFC } from 'react';
508 | import { Toggleable } from './Toggleable';
509 | import { PanelItemProps, PanelItem } from './PanelItem';
510 |
511 | const ToggleableOfPanelItem = Toggleable.ofType();
512 |
513 | const PanelViaInjection: SFC = ({title, children}) => (
514 |
515 | {children}
516 |
517 | );
518 | ```
519 |
520 | 所有的功能都能像原来的代码一样工作,并且现在 `props` 属性也支持 TS 类型检查了,很棒有木有! :smiley:
521 |
522 | 
523 |
524 |
525 |
526 |
527 |
528 | ## 高阶组件
529 |
530 | 最后我们来看下 HOC 。前面我们已经实现了 Toggleable 的渲染回调模式,那么很自然的我们可以衍生出一个 HOC 组件。
531 |
532 | > 如果对 HOC 不熟悉的话,可以先看下 React 官方文档对于 [HOC](https://reactjs.org/docs/higher-order-components.html) 的说明。
533 |
534 |
535 |
536 | 先来看看定义 HOC 我们需要做哪些工作:
537 |
538 | - `displayName` (方便在 devtools 里面进行调试)
539 | - `WrappedComponent ` (可以访问原始的组件 -- 有利于调试)
540 | - 引入 [hoist-non-react-statics](https://github.com/mridgway/hoist-non-react-statics) 包,将原始组件的静态方法全部“复制”到 HOC 组件上
541 |
542 |
543 |
544 | 下面直接上代码 -- `withToggleable` 高阶组件:
545 |
546 | 
547 |
548 |
549 |
550 | 现在我们来用 HOC 重写一个 Panel :
551 |
552 | ```typescript
553 | import { PanelItem } from './PanelItem';
554 | import withToggleable from './withToggleable';
555 |
556 | const PanelViaHOC = withToggleable(PanelItem);
557 | ```
558 |
559 |
560 |
561 | 然后,又可以实现折叠面板了 :smile:
562 |
563 | ```typescript
564 | class Collapse extends Component {
565 |
566 | render() {
567 | return (
568 |
569 |
内容1
570 |
内容2
571 |
572 | );
573 | }
574 | }
575 | ```
576 |
577 |
578 |
579 | ## 尾声
580 |
581 | 感谢能坚持看完的朋友,你们真的很棒!
582 |
583 | 如果觉得还不错请帮忙给个 :star:
584 |
585 |
586 |
587 | 最后,感谢 Anders Hejlsberg 和所有的 TS 贡献者 :thumbsup:
588 |
589 |
--------------------------------------------------------------------------------
/images/button-with-default-props.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/button-with-default-props.png
--------------------------------------------------------------------------------
/images/generic-toggleable-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/generic-toggleable-demo.gif
--------------------------------------------------------------------------------
/images/hoc-toggleable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/hoc-toggleable.png
--------------------------------------------------------------------------------
/images/inject-component-toggleable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/inject-component-toggleable.png
--------------------------------------------------------------------------------
/images/render-callback-collapse-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/render-callback-collapse-demo.gif
--------------------------------------------------------------------------------
/images/render-callback-collapse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/render-callback-collapse.png
--------------------------------------------------------------------------------
/images/render-callback-panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/render-callback-panel.png
--------------------------------------------------------------------------------
/images/render-callback-toggleable-children.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/render-callback-toggleable-children.png
--------------------------------------------------------------------------------
/images/render-callback-toggleable-render.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/render-callback-toggleable-render.png
--------------------------------------------------------------------------------
/images/render-callback.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/render-callback.png
--------------------------------------------------------------------------------
/images/state-readonly.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/state-readonly.gif
--------------------------------------------------------------------------------
/images/stateful.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/stateful.png
--------------------------------------------------------------------------------
/images/stateless.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/stateless.png
--------------------------------------------------------------------------------
/images/with-default-props.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepfunc/ts-react-component-patterns/6ccbf409b3a48a11cca3355ea8cecdfd0e297093/images/with-default-props.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ts-react-component-patterns",
3 | "version": "1.0.0",
4 | "author": "Xie Kai <6261625@qq.com>",
5 | "license": "MIT",
6 | "scripts": {
7 | "build": "webpack --colors --config ./webpack.config.js",
8 | "build-watch": "webpack --watch --colors --config ./webpack.config.js"
9 | },
10 | "dependencies": {
11 | "babel-polyfill": "^6.23.0",
12 | "babel-runtime": "6.26.0",
13 | "classnames": "^2.2.5",
14 | "debug": "~2.2.0",
15 | "ejs": "^2.5.7",
16 | "hoist-non-react-statics": "^2.5.0",
17 | "koa": "^2.5.1",
18 | "koa-mount": "^3.0.0",
19 | "koa-static": "^4.0.2",
20 | "koa-views": "^6.1.3",
21 | "react": "^16.3.2",
22 | "react-dom": "^16.3.2"
23 | },
24 | "devDependencies": {
25 | "@types/classnames": "^2.2.3",
26 | "@types/koa": "^2.0.45",
27 | "@types/koa-mount": "^2.0.19",
28 | "@types/koa-static": "^3.0.2",
29 | "@types/koa-views": "^2.0.3",
30 | "@types/react": "^16.3.14",
31 | "@types/react-dom": "^16.0.5",
32 | "autoprefixer": "^7.1.1",
33 | "babel-cli": "6.26.0",
34 | "babel-core": "6.26.0",
35 | "babel-loader": "7.1.2",
36 | "babel-plugin-transform-runtime": "6.23.0",
37 | "babel-preset-env": "1.6.1",
38 | "babel-preset-react": "6.24.1",
39 | "babel-preset-stage-0": "6.24.1",
40 | "babel-register": "6.26.0",
41 | "css-loader": "^0.28.7",
42 | "del": "^2.2.2",
43 | "html-webpack-plugin": "^2.24.1",
44 | "postcss-loader": "^2.0.6",
45 | "style-loader": "^0.13.1",
46 | "ts-loader": "^3.5.0",
47 | "typescript": "^2.9.1",
48 | "webpack": "^3.11.0"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Demo.tsx:
--------------------------------------------------------------------------------
1 | import React, { SFC, Fragment } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Button from './stateless/Button';
4 | import Collapse from './renderCallback/Collapse';
5 | import CollapseWithPanelViaInjection from './injectedComponent/Collapse';
6 | import CollapseWithGeneric from './generic/Collapse';
7 | import CollapseWithHOC from './hoc/Collapse';
8 |
9 | const Demo: SFC = () => (
10 |
11 | 折叠面板:渲染回调模式
12 |
13 | 折叠面板:组件注入模式
14 |
15 | 折叠面板:泛型组件
16 |
17 | 折叠面板:HOC
18 |
19 |
20 | );
21 |
22 | ReactDOM.render(, document.getElementById('root'));
--------------------------------------------------------------------------------
/src/defaultProps/ButtonWithDefaultProps.tsx:
--------------------------------------------------------------------------------
1 | import React, { SFC, MouseEvent } from 'react';
2 | import { withDefaultProps } from '../utils';
3 |
4 | const defaultProps = {color: 'blue'};
5 | type DefaultProps = typeof defaultProps;
6 |
7 | type Props = { onClick(e: MouseEvent): void; } & DefaultProps;
8 |
9 | const Button: SFC = ({onClick: handleClick, color, children}) => (
10 |
11 | );
12 |
13 | export default withDefaultProps(defaultProps, Button);
--------------------------------------------------------------------------------
/src/generic/Collapse.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PanelViaInjection from './PanelViaInjection';
3 |
4 | class Collapse extends Component {
5 |
6 | render() {
7 | return (
8 |
9 |
内容1
10 |
内容2
11 |
内容3
12 |
内容4
13 |
内容5
14 |
15 | );
16 | }
17 | }
18 |
19 | export default Collapse;
--------------------------------------------------------------------------------
/src/generic/PanelItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { SFC } from 'react';
2 | import { ToggleableComponentProps } from './Toggleable';
3 |
4 | type PanelItemProps = { title: string };
5 |
6 | const PanelItem: SFC = props => {
7 | const {title, children, show, toggle} = props;
8 |
9 | return (
10 |
11 |
{title}
12 | {show ? children : null}
13 |
14 | );
15 | };
16 |
17 | export { PanelItemProps, PanelItem };
--------------------------------------------------------------------------------
/src/generic/PanelViaInjection.tsx:
--------------------------------------------------------------------------------
1 | import React, { SFC } from 'react';
2 | import { Toggleable } from './Toggleable';
3 | import { PanelItemProps, PanelItem } from './PanelItem';
4 |
5 | const ToggleableOfPanelItem = Toggleable.ofType();
6 |
7 | const PanelViaInjection: SFC = ({title, children}) => (
8 |
9 | {children}
10 |
11 | );
12 |
13 | // TS 2.9 可以直接这样写
14 | // const PanelViaInjection: SFC = ({title, children}) => (
15 | // component={PanelItem} props={{title}}>
16 | // {children}
17 | //
18 | // );
19 |
20 | export default PanelViaInjection;
--------------------------------------------------------------------------------
/src/generic/Toggleable.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, ComponentType, ReactNode, MouseEvent } from 'react';
2 | import { isFunction } from '../utils';
3 |
4 | type DefaultInjectedProps = { props: P };
5 | const defaultInjectedProps: DefaultInjectedProps = {props: {}};
6 |
7 | type Props
= Partial<{
8 | children: RenderCallback | ReactNode;
9 | render: RenderCallback;
10 | component: ComponentType>
11 | } & DefaultInjectedProps>;
12 |
13 | type RenderCallback = (args: ToggleableComponentProps) => ReactNode;
14 | type ToggleableComponentProps
= {
15 | show: boolean;
16 | toggle: Toggleable['toggle'];
17 | } & P;
18 |
19 | const initialState = {show: false};
20 | type State = Readonly;
21 |
22 | class Toggleable extends Component, State> {
23 |
24 | static ofType() {
25 | return Toggleable as Constructor>;
26 | }
27 | static readonly defaultProps: Props = defaultInjectedProps;
28 | readonly state: State = initialState;
29 |
30 | render() {
31 | const {component: InjectedComponent, children, render, props} = this.props;
32 | const {show} = this.state;
33 | const renderProps = {show, toggle: this.toggle};
34 |
35 | if (InjectedComponent) {
36 | return (
37 |
38 | {children}
39 |
40 | );
41 | }
42 |
43 | if (render) {
44 | return render(renderProps);
45 | } else if (isFunction(children)) {
46 | return children(renderProps);
47 | } else {
48 | return null;
49 | }
50 | }
51 |
52 | private toggle = (event: MouseEvent) => {
53 | this.setState(prevState => ({show: !prevState.show}));
54 | };
55 | }
56 |
57 | export { ToggleableComponentProps, Toggleable };
--------------------------------------------------------------------------------
/src/hoc/Collapse.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PanelViaHOC from './PanelViaHOC';
3 |
4 | class Collapse extends Component {
5 |
6 | render() {
7 | return (
8 |
12 | );
13 | }
14 | }
15 |
16 | export default Collapse;
--------------------------------------------------------------------------------
/src/hoc/PanelItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { SFC } from 'react';
2 | import { ToggleableComponentProps } from './Toggleable';
3 |
4 | type PanelItemProps = { title: string };
5 |
6 | const PanelItem: SFC = props => {
7 | const {title, children, show, toggle} = props;
8 |
9 | return (
10 |
11 |
{title}
12 | {show ? children : null}
13 |
14 | );
15 | };
16 |
17 | export { PanelItemProps, PanelItem };
--------------------------------------------------------------------------------
/src/hoc/PanelViaHOC.tsx:
--------------------------------------------------------------------------------
1 | import { PanelItem } from './PanelItem';
2 | import withToggleable from './withToggleable';
3 |
4 | const PanelViaHOC = withToggleable(PanelItem);
5 |
6 | export default PanelViaHOC;
--------------------------------------------------------------------------------
/src/hoc/Toggleable.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, ComponentType, ReactNode, MouseEvent } from 'react';
2 | import { isFunction } from '../utils';
3 |
4 | type DefaultInjectedProps = { props: P };
5 | const defaultInjectedProps: DefaultInjectedProps = {props: {}};
6 |
7 | type Props
= Partial<{
8 | children: RenderCallback | ReactNode;
9 | render: RenderCallback;
10 | component: ComponentType
11 | } & DefaultInjectedProps>;
12 |
13 | type RenderCallback = (args: ToggleableComponentProps) => ReactNode;
14 | type ToggleableComponentProps = {
15 | show: boolean;
16 | toggle: Toggleable['toggle'];
17 | };
18 |
19 | const initialState = {show: false};
20 | type State = Readonly;
21 |
22 | class Toggleable extends Component, State> {
23 |
24 | static ofType() {
25 | return Toggleable as Constructor>;
26 | }
27 | static readonly defaultProps: Props = defaultInjectedProps;
28 | readonly state: State = initialState;
29 |
30 | render() {
31 | const {component: InjectedComponent, children, render, props} = this.props;
32 | const {show} = this.state;
33 | const renderProps = {show, toggle: this.toggle};
34 |
35 | if (InjectedComponent) {
36 | return (
37 |
38 | {children}
39 |
40 | );
41 | }
42 |
43 | if (render) {
44 | return render(renderProps);
45 | } else if (isFunction(children)) {
46 | return children(renderProps);
47 | } else {
48 | return null;
49 | }
50 | }
51 |
52 | private toggle = (event: MouseEvent) => {
53 | this.setState(prevState => ({show: !prevState.show}));
54 | };
55 | }
56 |
57 | export { ToggleableComponentProps, Toggleable };
--------------------------------------------------------------------------------
/src/hoc/withToggleable.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, ComponentType } from 'react';
2 | import hoistNonReactStatics from 'hoist-non-react-statics';
3 | import { Toggleable, ToggleableComponentProps } from './Toggleable';
4 | import { getHOCComponentName } from '../utils';
5 |
6 | function withToggleable(
7 | WrappedComponent: ComponentType) {
8 |
9 | type Props = Omit;
10 |
11 | class WithToggleable extends Component {
12 |
13 | static readonly displayName = getHOCComponentName('WithToggleable', WrappedComponent);
14 | static readonly WrappedComponent = WrappedComponent;
15 |
16 | render() {
17 | return (
18 | }/>
19 | );
20 | }
21 | }
22 |
23 | return hoistNonReactStatics(WithToggleable, WrappedComponent);
24 | }
25 |
26 | export default withToggleable;
--------------------------------------------------------------------------------
/src/injectedComponent/Collapse.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PanelViaInjection from './PanelViaInjection';
3 |
4 | class Collapse extends Component {
5 |
6 | render() {
7 | return (
8 |
9 |
内容1
10 |
内容2
11 |
内容3
12 |
13 | );
14 | }
15 | }
16 |
17 | export default Collapse;
--------------------------------------------------------------------------------
/src/injectedComponent/PanelItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { SFC } from 'react';
2 | import { ToggleableComponentProps } from './Toggleable';
3 |
4 | type PanelItemProps = { title: string };
5 |
6 | const PanelItem: SFC = props => {
7 | const {title, children, show, toggle} = props;
8 |
9 | return (
10 |
11 |
{title}
12 | {show ? children : null}
13 |
14 | );
15 | };
16 |
17 | export { PanelItemProps, PanelItem };
--------------------------------------------------------------------------------
/src/injectedComponent/PanelViaInjection.tsx:
--------------------------------------------------------------------------------
1 | import React, { SFC } from 'react';
2 | import { Toggleable } from './Toggleable';
3 | import { PanelItemProps, PanelItem } from './PanelItem';
4 |
5 | const PanelViaInjection: SFC = ({title, children}) => (
6 |
7 | {children}
8 |
9 | );
10 |
11 | export default PanelViaInjection;
--------------------------------------------------------------------------------
/src/injectedComponent/Toggleable.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, ComponentType, ReactNode, MouseEvent } from 'react';
2 | import { isFunction } from '../utils';
3 |
4 | const defaultInjectedProps = {props: {} as { [propName: string]: any }};
5 | type DefaultInjectedProps = typeof defaultInjectedProps;
6 | type Props = Partial<{
7 | children: RenderCallback | ReactNode;
8 | render: RenderCallback;
9 | component: ComponentType>
10 | } & DefaultInjectedProps>;
11 |
12 | type RenderCallback = (args: ToggleableComponentProps) => ReactNode;
13 | type ToggleableComponentProps = {
14 | show: boolean;
15 | toggle: Toggleable['toggle'];
16 | } & P;
17 |
18 | const initialState = {show: false};
19 | type State = Readonly;
20 |
21 | class Toggleable extends Component {
22 |
23 | static readonly defaultProps: Props = defaultInjectedProps;
24 | readonly state: State = initialState;
25 |
26 | render() {
27 | const {component: InjectedComponent, children, render, props} = this.props;
28 | const {show} = this.state;
29 | const renderProps = {show, toggle: this.toggle};
30 |
31 | if (InjectedComponent) {
32 | return (
33 |
34 | {children}
35 |
36 | );
37 | }
38 |
39 | if (render) {
40 | return render(renderProps);
41 | } else if (isFunction(children)) {
42 | return children(renderProps);
43 | } else {
44 | return null;
45 | }
46 | }
47 |
48 | private toggle = (event: MouseEvent) => {
49 | this.setState(prevState => ({show: !prevState.show}));
50 | };
51 | }
52 |
53 | export { ToggleableComponentProps, Toggleable };
--------------------------------------------------------------------------------
/src/renderCallback/Collapse.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Panel from './Panel';
3 |
4 | class Collapse extends Component {
5 |
6 | render() {
7 | return (
8 |
9 |
内容1
10 |
内容2
11 |
内容3
12 |
内容4
13 |
14 | );
15 | }
16 | }
17 |
18 | export default Collapse;
--------------------------------------------------------------------------------
/src/renderCallback/Panel.tsx:
--------------------------------------------------------------------------------
1 | import React, { SFC } from 'react';
2 | import Toggleable from './Toggleable';
3 |
4 | type Props = { title: string };
5 |
6 | const Panel: SFC = ({title, children}) => (
7 | (
9 |
10 |
{title}
11 | {show ? children : null}
12 |
13 | )}
14 | />
15 | );
16 |
17 | export default Panel;
18 |
19 |
--------------------------------------------------------------------------------
/src/renderCallback/Toggleable.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, ReactNode, MouseEvent } from 'react';
2 | import { isFunction } from '../utils';
3 |
4 | // 属性相关
5 | type Props = Partial<{
6 | children: RenderCallback;
7 | render: RenderCallback;
8 | }>;
9 | type RenderCallback = (args: ToggleableRenderArgs) => ReactNode;
10 | type ToggleableRenderArgs = {
11 | show: boolean;
12 | toggle: Toggleable['toggle'];
13 | };
14 |
15 | // 状态相关
16 | const initialState = {show: false};
17 | type State = Readonly;
18 |
19 | class Toggleable extends Component {
20 |
21 | readonly state: State = initialState;
22 |
23 | render() {
24 | const {children, render} = this.props;
25 | const {show} = this.state;
26 | const renderArgs = {show, toggle: this.toggle};
27 |
28 | if (render) {
29 | return render(renderArgs);
30 | } else if (isFunction(children)) {
31 | return children(renderArgs);
32 | } else {
33 | return null;
34 | }
35 | }
36 |
37 | private toggle = (event: MouseEvent) => {
38 | this.setState(prevState => ({show: !prevState.show}));
39 | };
40 | }
41 |
42 | export default Toggleable;
--------------------------------------------------------------------------------
/src/server/server.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import Koa from 'koa';
3 | import mount from 'koa-mount';
4 | import serve from 'koa-static';
5 | import views from 'koa-views';
6 |
7 | const cwd = process.cwd();
8 | const app = new Koa();
9 | const listenPort = 3000;
10 |
11 | app.use(mount('/dist', serve(path.join(cwd, 'dist'))));
12 |
13 | app.use(views(path.join(cwd, 'src/server/views'), {extension: 'ejs'}));
14 |
15 | app.use(mount('/', async ctx => await ctx.render('demo')));
16 |
17 | app.listen(listenPort, () => console.log('node server is listening on port: ' + listenPort));
--------------------------------------------------------------------------------
/src/server/views/demo.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ts-react-component-patterns-demo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/server/www.js:
--------------------------------------------------------------------------------
1 | require('babel-polyfill');
2 | require('babel-register');
3 | require('./server');
4 |
--------------------------------------------------------------------------------
/src/stateful/CounterButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | const initialState = {count: 0};
4 | type State = Readonly;
5 |
6 | class CounterButton extends Component<{}, State> {
7 |
8 | readonly state: State = initialState;
9 |
10 | render() {
11 | const {count} = this.state;
12 |
13 | return (
14 |
15 |
16 |
当前计数:{count}
17 |
18 | )
19 | }
20 |
21 | private handleIncrement = () => this.setState(prevState => ({count: prevState.count + 1}));
22 | }
23 |
24 | export default CounterButton;
25 |
26 |
--------------------------------------------------------------------------------
/src/stateless/Button.tsx:
--------------------------------------------------------------------------------
1 | import React, { SFC, MouseEvent } from 'react';
2 |
3 | interface Props {
4 | onClick(e: MouseEvent): void;
5 | }
6 |
7 | const Button: SFC = ({onClick: handleClick, children}) => (
8 |
9 | );
10 |
11 | export default Button;
--------------------------------------------------------------------------------
/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "moduleResolution": "node",
4 | "module": "es2015",
5 | "target": "es2015",
6 | "lib": [
7 | "es2015",
8 | "dom"
9 | ],
10 | "jsx": "preserve",
11 | "allowSyntheticDefaultImports": true,
12 | "strict": true,
13 | "baseUrl": "."
14 | },
15 | "include": [
16 | "./**/*"
17 | ]
18 | }
--------------------------------------------------------------------------------
/src/types/global.d.ts:
--------------------------------------------------------------------------------
1 | declare type DiffPropertyNames =
2 | { [P in T]: P extends U ? never: P }[T];
3 |
4 | declare type Omit = Pick>;
5 |
6 | declare type Constructor = { new(...args: any[]): T };
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { ComponentType } from 'react';
2 |
3 | function withDefaultProps(defaultProps: DP, Cmp: ComponentType) {
4 | // 首先将必填属性抽取出来
5 | type RequiredProps = Omit
;
6 | // 然后重新构造属性类型定义,可选的默认值属性 + 必填属性
7 | type Props = Partial & RequiredProps;
8 |
9 | // 把默认值设置好
10 | Cmp.defaultProps = defaultProps;
11 | // 返回处理好的组件类型
12 | return (Cmp as ComponentType) as ComponentType;
13 | }
14 |
15 | function isFunction(value: any): value is T {
16 | return typeof value === 'function';
17 | }
18 |
19 | function getComponentName(component: ComponentType) {
20 | return component.displayName || (component as any).name;
21 | }
22 |
23 | function getHOCComponentName(hocName: string, component: ComponentType) {
24 | return `${hocName}(${getComponentName(component)})`;
25 | }
26 |
27 | export { withDefaultProps, isFunction, getComponentName, getHOCComponentName };
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const rootDir = __dirname;
4 |
5 | module.exports = {
6 | context: rootDir,
7 |
8 | devtool: 'cheap-module-source-map',
9 |
10 | entry: {'demo': './src/Demo.tsx'},
11 |
12 | output: {
13 | path: path.join(rootDir, 'dist'),
14 | publicPath: '/dist/',
15 | filename: '[name].js',
16 | chunkFilename: '[name].js',
17 | },
18 |
19 | resolve: {
20 | extensions: ['.ts', '.tsx', '.js', '.jsx']
21 | },
22 |
23 | module: {
24 | rules: [
25 | {
26 | test: /\.css$/,
27 | use: ['style-loader', 'css-loader']
28 | },
29 | {
30 | test: /\.tsx?$/,
31 | use: [
32 | {
33 | loader: 'babel-loader',
34 | options: {
35 | cacheDirectory: true
36 | }
37 | },
38 | {
39 | loader: 'ts-loader',
40 | options: {
41 | configFile: path.join(rootDir, 'src/tsconfig.json'),
42 | }
43 | }
44 | ]
45 | }
46 | ]
47 | }
48 | };
--------------------------------------------------------------------------------