React HOC(Higher-order component)をTypeScriptで使う

ReactのComponentのリファクタリングなどの際に、HOC(Higher-order component)で
コンポーネントを拡張することがあるが、TypeScriptでの実装をするにあたりお試しした備忘録。

HOCのおさらい

HOC(Higher-order component)はcomponentを引数にとり、新しいコンポーネントを返す関数であり、コンポーネント間のコードを再利用可能にする際に利用される手法。

ざっくり以下のような感じのこと。

1
const composedComponent = hightOrderComponent(Component)

またはコンポーネントにpropsを渡すなら

1
const composedComponent = hightOrderComponent(injectProps)(Component)

という感じのもの。

以下の点が確保されている必要がある。

  • 純粋関数であり参照透過性を保持することで副作用を避ける。
  • HOC内でprototype拡張をしない。これはHOC利用側がHOCでのprototype変更の内部実装を知る必要が出てくる。
  • renderメソッドないでHOCを使用しない。render内でHOCを使うことで毎回新しいコンポーネントが生成されるため、パフォーマンスに影響するため。

TypeScriptでの実装

Proxy props

一般的なものはpropsをproxyするパターン

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
31
32
33
34
35
36
37
38
39
import React from 'react'

// HOCに渡すProps
type ExternalProps = {
age: number
}

export interface InjectedProps {
name: string
}

type Options = {
name: string
}

export const propsProxyHOC = ({ name = 'James' }: Partial<Options> = {}) => <OriginalProps extends {}>(
WrappedComponent: React.ComponentType<OriginalProps & InjectedProps>
) => {
class PropsProxyHOC extends React.Component<OriginalProps & ExternalProps> {
public render() {
const InjectedProps = {
name,
}
return (
<div>
<WrappedComponent {...this.props} {...InjectedProps} />
{this.props.age}
</div>
)
}
}

if (WrappedComponent.displayName) {
PropsProxyHOC.displayName = `${
WrappedComponent.displayName
}WrappedPropsProxyHOC`
}
return PropsProxyHOC
}
1
const propsProxyHOC = ({ name = 'James' }: Partial<Options> = {}) => <OriginalProps extends {}>

default引数を指定する。引数オブジェクトのnameメンバが省略されている場合は、default値(James)が渡される。
引数自体がない場合は空のオブジェクトが渡るようになる。
Partial keywordを使いOptionsのパラメーターをオプショナルにする。これにより引数オブジェクトのメンバを把握しておく必要性がなくなる。

つまりPartialにより引数の型は以下のようになる。

1
2
3
type Options = {
name?: string
}

render部分では

1
2
3
4
5
6
7
8
9
10
11
public render() {
const InjectedProps = {
name,
}
return (
<div>
<WrappedComponent {...this.props} {...InjectedProps} />
{this.props.age}
</div>
)
}

propsを追加してcomponentを拡張する。
また、componet自体を別要素でwrapすることもできる。

以下のようにして利用する。

1
2
3
4
5
6
7
const composedComponent = propsProxyHOC({ name: 'John' })(Component)

React.render(
return (
<composedComponent age={10} />
)
)

Inheritance

またprops proxyするだけではなく、inheritanceすることも可能である。

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
31
32
33
34
35
36
37
38
39
40
41
42
type ExternalProps = {
style?: React.CSSProperties
}

export interface InjectedProps {}

type Options = {
message: string
}

export const inheritanceInversionHOC = ({ message = 'default message' }: Partial<Options> = {}) =>
<OriginalProps extends {}>(WrappedComponent: React.ComponentType<OriginalProps & InjectedProps>) => {
class WrapedComponentHOC extends WrappedComponent<OriginalProps & ExternalProps> {
componentWillMount() {
if (super.componentWillMount) {
super.componentWillMount()
}
}

render() {
const injectProps = {
message
}
const elementsTree = <WrappedComponent {...this.props} {...injectProps} />
const { children, ...rest } = elementsTree.props

return (
<div {...rest}>
<h1>{injectProps.message}</h1>
{children}
</div>
)
}
}

if (WrappedComponent.displayName) {
WrapedComponentHOC.displayName = `${
WrappedComponent.displayName
}WithWrapedComponentHOC`
}
return WrapedComponentHOC
}

継承することでComponentのlifecycleにアクセスできるようになる。

ということTypescriptでのReact HOCのパターンを触れてみた。

参考にしたページ

ReactのHigher Order Components詳解 : 実装の2つのパターンと、親Componentとの比較

Comments