TypeScriptで書くHOC 〜OriginalPropsを添えて〜

React Hooksの登場でやや影の薄くなりつつあるHOC(Higher Order Component)ですが、まだまだ登場機会がなくなった訳ではありません。

TypeScriptで書くHOCは型定義のシンタックスがごちゃごちゃしてわかりづらくなりがちなので、「英語・日本語出し分け用のHOCを作ってみる」というケースをイメージしつつ、改めて整理してみました。

なお、returnを完全に省略したときのHOCのシンタックスの一見さんお断り感は異常なので、適宜returnを残した状態の記述にしています。

## やりたいこと
コンポーネントに言語設定を適用したい。
具体的には、アプリケーションのトップレベルで指定されている`lang`というpropsを任意のコンポーネントから参照して、`'en'`のときと`'ja'`のときとで表示するテキストを変更するなどしたい。

## 作戦
`LangProvider`というコンポーネントを用意し、アプリケーションのトップレベルで使用する。
`withLang`というHOCを用意し、この引数に与えられたコンポーネントからは`lang`の値を参照できるようにする。

## 使用イメージ
まず始めに、`withLang`（と`LangProvider`）の使用方法をイメージしてみます。

```tsx
const App = () => (
  <LangProvider lang='ja'>
    <Hello />
  </LangProvider>
)
```

```tsx
export const Hello: React.FC<LangProps> = withLang(({ lang }) => {
  const text = lang === 'ja' ? 'こんにちは、世界。' : 'Hello, world.'
  
  return (
    <div>
      <h1>{text}</h1>
    </div>
  )
})
```

こんな風に、`withLang`というHOCの引数に与えられたコンポーネントでは、`LangProvider`で指定した`lang`にアクセスできるようになる、という算段です。

今回はあまり関係ありませんが、一応ディレクトリ構成も書いておきます。

```
src
├── App.tsx
├── components
│   ├── Hello.tsx
│   └── index.ts
├── index.tsx
└── lang
    ├── index.ts
    ├── lang.tsx
    └── withLang.tsx
```

## コードを書いてみる

### Step1: `LangProvider`を用意する

`lang.tsx`内に、`LangProvider`コンポーネント（と諸々の型定義）を作ります。

```tsx:lang.tsx
import React from 'react'

export interface LangProps {
  lang?: 'en' | 'ja'
}

const defaultLang: LangProps = {
  lang: 'en',
}

const setLang = (lang: LangProps = {}): LangProps => ({
  ...defaultLang,
  ...lang,
})

export const LangContext = React.createContext<LangProps>(setLang())

export const LangProvider: React.FC<LangProps> = ({ children, ...props }) => {
  return (
    <LangContext.Provider value={setLang({ ...props } || {})}>
    {children}
    </LangContext.Provider>
  )
}
```

普通にProviderを作っているだけなので、特に解説はありません。
`lang`を引数として与えなかった場合はデフォルトで`'en'`になるんだな程度の理解でOKです。

### Step2: `withLang`を用意する

次に、今回のポイントである`withLang`を用意しましょう。

コードを書き始める前に、もう一度`withLang`の最終的な使用イメージを確認しておきます。

（前述と同じコード）

```tsx:Hello.tsx
const Hello: React.FC<LangProps> = withLang(({ lang }) => {
  const text = lang === 'ja' ? 'こんにちは、世界。' : 'Hello, world.'
  
  return (
    <div>
      <h1>{text}</h1>
    </div>
  )
})
```

`withLang`の引数はFunctional Componentで、返り値もFunctional Componentということになります。

では、`withLang`を書いていきましょう。

素直に書いていくと、こんな感じになるのではないでしょうか。（**このコードには改善の余地があります。後述。**）

```tsx:withLang.tsx
import React from 'react'
import { LangContext, LangProps } from './lang'

export const withLang = (WrappedComponent: React.ComponentType<LangProps>) => {
  return (): React.ReactElement => (
    <LangContext.Consumer>
      {({ lang }: LangProps): React.ReactElement => <WrappedComponent lang={lang} />}
    </LangContext.Consumer>
  )
}
```

`withLang`の引数となるFunctional Componentを`WrappedComponent`という名前でとり、返り値としてFunctional Componentを返しています。
返り値のFunctional Component内では引数のコンポーネント（=`WrappedComponent`）を`LangContext.Consumer`でラップし、`lang`にアクセスできるようにした上でpropsとして渡しています。

これで`<LangProvider>`に`lang='ja'`のように引数を渡せば、`Hello`コンポーネント内でも`lang`を参照できるようになりました。

ちなみに`React.ComponentType<P>`というのは、`ComponentClass<P> | StatelessComponent<P>`の型で、要するにClass ComponentとFunctional Componentのどちらも解決できるという型です。

### Step3: `withLang`を修正する

さて、前述の状態だと、実は問題が一つあります。

それは、HOCがネスト状態になった時に、propsが正しく渡されないという問題です。

どういうことでしょうか。

例えば、`withNightMode`のようなHOCを新たに用意して、ナイトモードの場合デザインが変わるような機能をコンポーネントに与えたいとします。

最終的な使用イメージとしては、こんな感じです。

```tsx:Hello.tsx
const HelloComponent: React.FC<HelloProps> = ({ lang, nightMode }): React.ReactElement => {
  // nightModeがtrueのときとfalseのときで背景のグラデーションを変える
  const background = nightMode
    ? 'linear-gradient(45deg, #485563, #29323c)'
    : 'linear-gradient(45deg, #fe8c00, #f83600)'

  const text = lang === 'ja' ? 'こんにちは、世界。' : 'Hello, world.'

  return (
    <div style={{ background }}>
      <h1>{text}</h1>
    </div>
  )
}

export const Hello = withNightMode(withLang(HelloComponent)) // withNightModeにwithLangを渡している
```

先ほど作った`withLang`を、さらに`withNightMode`に渡すことで、最終的に返されるコンポーネントから`lang`、`nightMode`のどちらにもアクセスしようとしています。

`withNightMode`の実装は省略しますが、`withLang`とほぼ同じだと思ってください。

そして`App.tsx`内で下記のように`NightModeProvider`のコンポーネントを使うと、一見`<Hello />`にnightModeが適用されそうな気がします。

```tsx:App.tsx
const App: React.FC = () => {
  return (
    <NightModeProvider nightMode={true}>
      <LangProvider lang={'ja'}>
        <Hello />
      </LangProvider>
    </NightModeProvider>
  )
}
```

しかし実際は、この状態だと`Hello`コンポーネント内でアクセスできる`nightMode`は`undefined`となってしまいます。

その理由は、**`withNightMode`によって渡されるpropsが`withLang`内では無視され、末端のコンポーネントには渡されていないから**です。

というのも、`withLang`の実装をもう一度見てみると、`lang`こそ`WrappedComponent`へのpropsとして渡されているものの、それ以外のpropsに関する記述はありません。

（前述と同じコード）

```tsx:withLang.tsx
import React from 'react'
import { LangContext, LangProps } from './lang'

export const withLang = (WrappedComponent: React.ComponentType<LangProps>) => {
  return (): React.ReactElement => (
    <LangContext.Consumer>
      {({ lang }: LangProps): React.ReactElement => <WrappedComponent lang={lang} />}
    </LangContext.Consumer>
  )
}
```

つまり、**`withNightMode`のHOCによって`nightMode`のpropsが`withLang`に渡されても、`withLang`的にはそんなこと知らないよ**、ということですね。

そこで、`withLang`自体に渡されるprops（今回で言えば`nightMode`）も`WrappedComponent`に渡されるように、`withLang`を修正していきます。

完成系はこうなります。

```tsx:withLang.tsx
import React from 'react'
import { LangContext, LangProps } from './lang'

export const withLang = <OriginalProps extends {}>(
  WrappedComponent: React.ComponentType<OriginalProps & LangProps>
) => {
  return (props: OriginalProps): React.ReactElement => (
    <LangContext.Consumer>
      {({ lang }: LangProps): React.ReactElement => (
        <WrappedComponent lang={lang} {...props} />
      )}
    </LangContext.Consumer>
  )
}
```

ややシンタックスがわかりづらいので、分けて説明します。

まずは前半部分。

```tsx:withLang.tsx
export const withLang = <OriginalProps extends {}>(
  WrappedComponent: React.ComponentType<OriginalProps & LangProps>
) => {
  // 略
}
```

前半の説明としては、

- `withLang`に渡されるpropsの型を`OriginalProps`というジェネリックで指定。
- 最終的に返すコンポーネントのpropsの型に、`OriginalProps`をintersection形式で追加

ということになります。  
つまり、今回でいえば**`withNightMode`から渡されるpropsを`withLang`内で`OriginalProps`型（の一つの値）として受け取り、それを最終的に返すコンポーネント（`WrappedComponent`）に含めるように型を指定している**、ということですね。

ちなみに`<OriginalProps extends {}>`の`extends {}`は、特に機能的な役割を果たしている訳ではなく、「JSXのシンタックスではないよ」というのを指示するための決まり文句のようなものです。

次に、後半部分です。

```tsx:withLang.tsx
// 略
  return (props: OriginalProps): React.ReactElement => (
    <LangContext.Consumer>
      {({ lang }: LangProps): React.ReactElement => (
        <WrappedComponent lang={lang} {...props} />
      )}
    </LangContext.Consumer>
  )
// 略
```

ここでは、

- 実際にpropsを受け取る
- 最終的に返されるコンポーネントにpropsを渡す

という作業を追加しています。

今回のケースで言えば、**`nightMode`などのpropsを受け取り、それをそのままコンポーネントに渡している（`{...props}`）**、ということですね。

上記のような作りにすることで、HOCがネストされても、全てのpropsが末端のコンポーネントまで正しく渡されるようになります。

これで完璧なHOCの完成です。

## ソースコード全容

ソースコードは[https://github.com/Tokky0425/ts-hoc](https://github.com/Tokky0425/ts-hoc)に置いてあるので、全体を見たい場合はこちらから見てください。

今回のようなProviderを使ったパターンの場合は`React.useContext`を使ってHooksで実装した方がシンプルかつ綺麗に書けますが、styled-componentsを使うときなど、あえてpropsとして何かしらの値を受け取りたい場合はHOCが便利です。

---

## 以下、基本的なパターンチートシート

TypeScriptでの基本的なHOCの雛形は下記のようになります。

### Enhancerパターン

「とりあえずpropsを増やす（拡張する）」というパターンです。
例えば、`loading`などのように、他の場所でも同じ型定義で使い回しそうなpropsを追加したいときに便利です。

```tsx
interface EnhencedProps {
  someProp: any
}

export const withSomething = <OriginalProps extends {}>(
  WrappedComponent: React.ComponentType<OriginalProps & EnhencedProps>
) => {
  return (props: OriginalProps & EnhencedProps) => <WrappedComponent {...props} />
}
```

### Injectorパターン

目的のコンポーネントに任意のpropsを注入できるようにするパターンです。
基本的な仕組みとしてはEnhancerパターンと同じで、propsの枠だけ与えるか、値も与えちゃうかだけの違いです。
記事内で実装したものも、Providerを用いているため若干構造が雛形とは違いますが、カテゴリ的にはこちらですね。

react-reduxの`connect`関数でお馴染みですが、`withSomething({someProp: someValue})(TargetComponent)`のように引数を取れるようにもできるのがポイントです。
（雛形のコードは引数を取るパターンにしています）

```tsx
interface ExternalProps {
  someProp: any
}

interface InjectedProps {
  someFunction: any
}

export const withSomething = ({ someProp }: ExternalProps) => {
  return <OriginalProps extends {}>(
    WrappedComponent: React.ComponentType<OriginalProps & InjectedProps>
  ) => {
     // whatever
    const someFunction = () => {
      console.log(someProp)
    }
    
    return (props: OriginalProps) => <WrappedComponent someFunction={someFunction} {...props} />
  }
}
```

上記のコードでは`someFunction`という関数をpropsに渡すようなHOCになっていますが、使い方としては自由なのでなんでもできます。

---
## 所感
できるだけHooksで済ませよう。