node プロジェクトの docker image をつくってみる。

社内でAWS ECS を使うことになったが、そもそも dokcer image を作ったことがなかったので
調べてみた。

目次

  1. node プロジェクトの用意
  2. Dockerfile の作成と build
  3. image からコンテナ起動
  4. docker-compose 起動

1. node プロジェクトの用意

typescript & Express を利用した簡単な web サーバを用意する。 port 番号で 3000 で起動して、localhost:3000/api に HTTP GET リクエストするとslack に通知がくるものだ。

フォルダ構成は以下の通り

.
├── __test__
├── jest.config.js
├── node_modules
├── nodemon.json
├── package-lock.json
├── package.json
├── src
└── tsconfig.json

2. Dockerfile とコンテナイメージの作成

alpine という軽量 Linux ディストリビューションを利用する。 この上に nodejs と npm をインストールし、ローカルで開発した node プロジェクトを含めるようにする。そして、npm installnpm startを起動させている。

touch Dockerfileでカレントディレクトリにファイルを作成して次の通り記述する。

Dockerfile

FROM alpine:latest

# instal llatest nodejs and npm
RUN apk add --update nodejs nodejs-npm

# make entry directory
RUN mkdir myapp
WORKDIR /myapp

# copy localfile to images
ADD src/ src/
ADD nodemon.json .
ADD package.json .
ADD tsconfig.json .
RUN npm set progress=false && \
  npm install
CMD npm start

npm set progress=falseは本質的ではないので、なくてもよい。
これはnpm installの進捗報告を出力しない設定で起動を速くしているために記述している。

Dockerfile があるディレクトリから以下を実行する
docker build -t tsweb .

-t オプションはイメージに名前をつける。tag の略称だ。

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
tsweb               latest              1a656139af79        6 seconds ago       32.9MB
alpine              latest              4d90542f0623        3 days ago          5.58MB

3. コンテナ起動

以下のコマンドを叩く。 docker run -d --name web -p 8000:3000 tsweb

curl localhost:8000/apiと叩けば、次の通りslack通知がくる。 f:id:poppon555:20190623220312p:plain

4. docker-compose 起動

蛇足になるが、docker-compose を利用して同じようにコンテナを起動してみる。

docker-compse.yml

web:
  image: tsweb
  ports:
    - '8000:3000'

docker-compose up -dでバックグラウンドでコンテナ起動。

最後に

ローカル環境で docker image を作成してコンテナ起動までの手順を紹介した。
今度は ECR に image を登録して、ECS で利用してみるところまで紹介したい。

参考にしたサイト

docker run -vとDockerfileのVOLUMEは違うのか?

はじめに

最近dockerについてプログラマのためのDocker教科書(以下教科書と呼ぶ)で勉強し始めた。

今回は、dockerを使ってデータの永続化という話でつまづいたときの整理をしたいと思う。

データの永続化とは、たとえばmysqlコンテナとデータコンテナを分けるということである。
たとえ前者を削除しても、データコンテナは別なのでデータを失うことはない。 別のバージョンのmysqlをデータコンテナにリンクさせることでポータビリティを保てるというメリットがあるわけだ。

教科書でデータ用のコンテナを作成するときに、次のDockerfileからイメージをビルドして作成している。

Dockerfile

# Dockerイメージの取得
FROM centos:latest

...(中略)...

# ボリュームの作成
VOLUME /var/log/httpd

しかしここでいくつか気になる疑問がでてきたのだ。
- ファイル内のVOLUMEが指すものは、どこに存在するものなのか?(ホストのどこか?)
- Dokcerfileでイメージをビルドしないとデータ永続用コンテナは作成できないのか?
- docker runのvオプションとはVOLUMEは別物なのか?

この疑問に関して調べた結果を残そうと思う。 先に結論を述べると、vオプションでもVOLUMEも同じである。

docker run vオプションについて

教科書には、vオプションについて次のように書かれてある。

docker run -v [ホストのディレクトリ]:[コンテナのディレクトリ]
ホストとコンテナでディレクトリを共有化する方法。

なるほど、この場合データの実態はホストに存在している。

では、次のように書くと何を意味するのか?
docker run -v /myvol -e MYSQL_ROOT_PASSWORD=mysql --name dbserver mysql

下図の通り確かにmyvolが作成されたコンテナに存在している。
が、それはどこに繋がれてる? ホストなのか?

f:id:poppon555:20190504112607p:plain

実は、/myvolは、docker volumeに接続されている。 docker volume lsと入力すると、dockerで作成されたボリュームができる。

f:id:poppon555:20190504112714p:plain

ただし-v /myvolとした場合、volume名がハッシュ化されてどの用途のボリュームなのかがわからなくなる。上図でも一つはmysqlコンテナ自体が使用しているボリュームで、もう一つは/myvolにマウントしているボリュームになるが、ハッシュ化されたVOLUME NAMEではわからない。

名前解決できない状況を回避するためには、ボリューム名を明示的に指定して作成し、それをマウントさせる。
docker volume create v1
docker run -v v1:/myvol -name dbserver mysql

v1という名前のボリュームの作成

f:id:poppon555:20190504113741p:plain

作成したボリュームをマウントする docker run -d --name dbserver -v v1:/myvol -e MYSQL_ROOT_PASSWORD=root mysql

f:id:poppon555:20190504112737p:plain

docker volume lsで確認

f:id:poppon555:20190504112752p:plain

以上よりdocker runのvオプションについてまとめると、
docker run -v [ホストのディレクトリ]:[コンテナのディレクトリ] mysql ホストと共有化
docker run -v [dokcer volume]:[コンテナのディレクトリ] mysql 名前付きボリュームを指定ディレクトリにマウント
docker run -v [コンテナのディレクトリ] mysql ハッシュ値のボリュームを指定したディレクトリにマウント
となり、最後の2つについては、起動コンテナと別のボリュームがマウントされ、名前付きかどうかが異なる

補足: rmオプションについて
docker rmでコンテナを削除しても、vオプションで作られたボリュームは同時に削除されない。

f:id:poppon555:20190504112808p:plain

コンテナが不要になったと同時に削除するには、rmオプションをつけて、docker run --rm -v v1:/myvol mysql と入力する。

f:id:poppon555:20190504112821p:plain

※それでもマウントしたv1ボリュームは削除されないことは注意

.DockerFileのVOLUMEについて

教科書には、VOLUMEについてこう書かれている。

VOLUME ["/マウントポイント"]
指定したマウントポイントをコンテナ内に作成し、ホストやその他コンテナからボリュームの外部マウントを行います

よくわかりませんね。。。
なので、実際にDockerfileをつくってやってみましょう。

.DockerFile

FROM mysql
VOLUME /myvol
ENV MYSQL_ROOT_PASSWORD mysql

docker build -t dbesrver .でイメージ作成して、
docker run -d -name db dbserverでコンテナ起動
マウント状況を確認すると、

f:id:poppon555:20190504112837p:plain

上図の通り、Dockerfileを利用した場合も新たにボリュームを作成して、VOLUMEで指定したディレクトリにマウントするという意味である。ボリュームに名前をつけていないので、起動したコンテナのボリュームの名前をハッシュ値となる。 (したがって、ボリュームが不要ならば--rmオプションを付けた起動したほうがよい。)

結果

以上まとめて、はじめの疑問に回答すると

  1. ファイル内のVOLUMEが指すものは、どこに存在するものなのか?(ホストのどこか?)

    ホストに存在するが特定のディレクトリと共有してるわけではなく、
    docker volumeとして作成された新たにボリュームを指している

  2. Dokcerfileでイメージをビルドしないとデータ永続用コンテナは作成できないのか?

    docker run -vからも作成可能

  3. docker runのvオプションとはVOLUMEは別物なのか?

    同じである。 名前付きかどうかという違いがあるぐらいだ。

というわけでした。

参考サイト

詳しく書かれてあったのでわかりやすかったです。

https://qiita.com/namutaka/items/f6a574f75f0997a1bb1d https://qiita.com/mtgto/items/aabc212a1d3f99878828 https://qiita.com/gounx2/items/23b0dc8b8b95cc629f32

jsでオブジェクト配列をつかった集合演算

はじめに

jsで集合演算についてしらべてみた。
調べた限り集合演算の例は[1,2,3]と[2,3,4]の積集合をもとめるといったプリミティブ値の配列が多くて、オブジェクト配列をつかった 集合演算が見つからなかったので、残して見ようと思う。

問題(特定の本の返却期日をしらべる)

たとえば、ある人が本屋から複数の本をレンタルしてたときを考えてみる。
レンタルしている本は、id、名前とサイズともつ(A)
本屋はレンタルした本のidと返却期日を保管している(B)

レンタルしてる本からサイズが小さいものの返却期日を調べるにはどうすればよいか?
(A)の部分集合の返却期日を(B)から得たいというわけです。

これらをコードで記述すれば、

// (A) レンタルしてる本一覧
const books = [
  { id: 1, name: 'book1', size: 'large' },
  { id: 2, name: 'book2', size: 'small' },
  { id: 3, name: 'book3', size: 'medium' },
  { id: 4, name: 'book4', size: 'medium' },
  { id: 5, name: 'book5', size: 'small' },
];

// (B) レンタルしている本の返却期日一覧
const lends = [
  { id: 1, end_on: '2019-01-01' },
  { id: 2, end_on: '2019-02-01' },
  { id: 3, end_on: '2019-01-11' },
  { id: 4, end_on: '2019-01-22' },
  { id: 5, end_on: '2019-01-11' },
];

サイズがsmallのものは、idが2,5である。
これの返却期日をlendsから得ると次のとおりになる。

// もとめたいもの(サイズがsmall)
[
  { id: 2, end_on: '2019-02-01' },
  { id: 5, end_on: '2019-01-11' },
]

解決方法

books
  .filter(book => book.size === 'small')  // (1)
  .map(target => lends.find(lend => lend.id === target.id)) // (2)

(1): booksから特定サイズの部分集合をえる

(2): (1)で得た部分集合のidと同じidをもつものをfindで取得してmapで配列を返す

さいごに

jsでオブジェクト配列から積集合?をもとめる書き方を紹介してみました。

使えるときがあるのかな笑
SQLで書くと、

select L.id, L.end_on from books B where size = 'small' left join lends L on B.id = L.id

を求めたいわけでした。

jsで集合演算というとSetという演算子がつかえるみたいですね。
Setをつかって今回の問題はとけるのかな...

flow-typedで型情報を管理しやすくしてみた

flowで型を導入後、毎回importするのが面倒だなと感じるようになってきた。
flow-typedをつかえば型情報をグローバルに定義できるようなので、これでimport文書く手間をカットしたり型情報をうまく管理できるのでは...

ということで今回は、flow-typedをつかって型情報を楽に管理できる方法について書き残そうと思う。

目次

  1. flow, eslintを使えるように準備
  2. flow-typedのインストール
  3. flow-typedディレクトリ作成と.flowconfigの記述
  4. 定義ファイルをつくって動作確認
  5. サードパーティライブラリの型定義を利用してみる

1. flow, eslintの準備

以前作成した記事通りに準備します。 本記事では、割愛させてもらいます。

2. flow-typedのインストール

npm i -D flow-typedを実行する

3. flow-typedディレクトリと設定ファイル

下図のようにrootディレクトリにflow-typedというディレクトリを作成する。
このディレクトリに型定義ファイルを保存していく。
自分で作成したものやreduxなどのサードパーティのライブラリものが保存される。

.
├── .eslintrc.json
├── .flowconfig
├── flow-typed <- つくりました
├── node_modules
├── package-lock.json
├── package.json
└── src

.flowconfig

[ignore]

[include]

[libs]
flow-typed
[lints]

[options]

[strict]

型定義ファイルを保存したディレクトリがどこか指定する。
上記のとおり[libs]に対して、先程作成したflow-typedディレクトリを記述する

4. 型定義ファイルをつくって動作確認

1. 直下にファイルを作成する

flow-typed/sampleType.js

type SampleType = {
  id: number,
  name: string
};

src/sample.js

const obj: SampleType = {
  id: '21',  <- (1)
  name: "SampleName"
};

sample.jsファイル内にimport type {SampleType} from './path'の記述はない
そして、(1) は数値ではないので、エラーとなる。

2. ディレクトリをきってもglobalにロードしてくれる

flow-typed/myModule/myType.js

declare type MyType = {
  uid: number,
  address: string,
  tel: number,
};

これでimport文なしに型チェックしてくれる。

4. サードパーティライブラリの型定義を利用してみる

./node_module/.bin/flow-typed install redux@4
で公開されたreduxの型情報をダウンロードできる。
ファイルは、flow-typedディレクトリに作成される。 あとは、3.の同様に使用できる。

しかし、この場合global空間を汚染させないように作られるため、import文は必要となる。

さいごに

でもflowよりもtypescriptのほうが、人気なんだよね...

参考サイト

flow公式
flow-typed

Prettierについて調査

最近エディタをsublimeからvscodeに変更しました。
その中でPrettierも適用することになったので、こいつのことを理解しようとおもうようになりました。

はじめはimport内のタブサイズが4になってたりと思うどおりに動かなくて断念かと思ったが、再インストールしてようやく想定したとおりに動いてくれたので、安心。

目次

  1. Prettierとはなにか?
  2. eslintと併用する方法
  3. なぜeslint --fixではダメなのか? わざわざprettierを利用する意味はなんだ?
  4. vscodeでprettierを利用する

まずは、npm scripts経由でprettierの挙動を確かめ、その後vscodeで設定する方法を紹介していきたい。

Prettierとはなにか?

Prettierは、ソースをいい感じに整形してくれるツールである。
linterと非常によく似たポジションだと思える。 これを使えばプロジェクト内のコーディングスタイルを統一的にできるわけだ。 自分であっても先週と今週の書き方が異なったりするのでその差をなくしてくれる。

eslintと併用する

早速Prettierをつかってみよう

1. インストール
何はともあれ、必要なものをインストール。 eslintのインストールと設定は、以前の記事をみてほしい。

npm i -D prettier
npm install eslint-config-prettier eslint-plugin-prettier -D

2. eslintの設定にprettierの設定を追加する

.eslintrc.json

{
  "extends": [
    "airbnb",
    "plugin:prettier/recommended" // <- prettierを使うことを宣言
  ],
  "rules": {
    "prettier/prettier": [
    "always",
    // prettierのオプションをここに入れる
    {
      "singleQuote": true,
      "trailingComma": "es5",
      "semi": true,
      "bracketSpacing": true
    }
    ]
  }
}

3. 動作確認 npm scriptsにprettierを登録して、npm run fmtで実行してみる。

修正前

// flow
const obj = {
  key1: 'singleQuote',  // 問題なくシングルウォートを利用してる
  key2: "doubleQuote",  // 誤ってダブルクォートを利用してる
    key3: "tabSize4",   // タブのサイズが4と異なる
  key4: "notComma"      // 最後に,がない
};

const {key1, key2} = obj;  // 中括弧内にスペースがない

// 無駄な改行がある

// 1行が長すぎる関数
function doSomething(argument1: Array<Object>, argument2: boolean, argument3: boolean): string {
  return 'result'   // 最後にセミコロンなし
}

修正後

// flow
const obj = {
  key1: 'singleQuote',
  key2: 'doubleQuote',
  key3: 'tabSize4',
  key4: 'notComma',
};

const { key1, key2 } = obj;

function doSomething(
  argument1: Array<Object>,
  argument2: boolean,
  argument3: boolean
): string {
  return 'result';
}

以上のようにコード上に問題はないが、みやすさ的な保証を担保してくれるわけだ。

なぜeslint --fixではダメなのか? 何が違うのか?

How does it compare to ESLint/TSLint/stylelint, etc.?にあるように、 linterが適用するルールには2つのタイプがあって、
prettierはその一方を楽にするもの。

2タイプというのは、
1. Formatting rules(eg: max-len, keyword-spacing, ...)
2. Code-quality rules(eg: no-unused-vars, no-extra-bind, ...)
のことであり、prettierが扱うのは前者のほうで、
コードの品質的に問題はないが、チーム内でのスタイルルールが異なる問題を自動的に解決してくれるものといわけだ。

たとえば公式の説明をつかえば、次のコードはコード上問題ない。

foo(reallyLongArg(), omgSoManyParameters(), IShouldRefactorThis(), isThereSeriouslyAnotherOne());

だからlinterで修正はされないが、読みにくさというものはのこる。 これをprettierならば、次のように整形してくれるわけだ。

foo( 
  reallyLongArg(), 
  omgSoManyParameters(), 
  IShouldRefactorThis(), 
  isThereSeriouslyAnotherOne() 
);

vscodeのprettierの設定

1. 拡張機能のインストール
ext install esbenp.prettier-vscode

2. prettier設定反映
設定方法は、以下の通り複数存在するが、今回は3の設定を紹介する. エディタに依存することなく、npm scriptsで確実に動作をするのを確認してから進められ、さらにeslintrcの設定をそのまま活かしたいからである。

  1. 基本設定 > 設定のsetting.jsonに記述
  2. プロジェクトの.prettier.jsonに記述
  3. eslintrc内のprettier設定箇所に記述

3. 保存時にprettier適用
最後に、ファイル保存時にprettierが自動的に実行されるように設定。

settings.json

  ...
  "prettier.eslintIntegration": true,
  "[javascript]": {
    "editor.formatOnSave": true
  },
  ...

以上で完了。あとはいつもどおりに書けばよい。 prettierのoptionについてはまた調べよう。

参考サイト
- Prettier 入門 ~ESLintとの違いを理解して併用する~
- 公式サイト

WebpackのdevServerを使ってもサーバと通信できるフロント開発がしたい

はじめに

自分はReactやReduxを使ってWebアプリを開発するときにWebpackのdevServerにお世話になる。devServerを使えば、ソース変更時に自動でブラウザをリロードしてくれて便利だからである。

しかし、devServerを使った場合、localhostで動かしてるwebサーバと通信できない。たとえば、devServerは、http://localhost:8080で起動する。
そこから、http://localhost:8000で動作するwebサーバとHTTP通信することは異なるオリジンにアクセスすることになってしまい通信は失敗する。
localhost:8080とlocalhost:8000は、異なるオリジンなので前者から後者に対して通信することは普通できないのである。

ローカルでwebサーバを動かす一方でフロント開発でdevServerを使ってるとき、フロントがサーバからデータ取得できるようにするにはどうすればよいのだろうかと壁にぶつかった。異なるオリジンであっても通信できるようにするにはどうすればよいのだろうかということである。

なので今回は、異なるオリジンであってもサーバと通信できる設定について備忘録を残そうと思う。

目次

  1. expressで簡単なwebサーバの準備
  2. 同一オリジンから通信できることの確認
  3. 別オリジンから通信の失敗確認
  4. expressのCORS設定
  5. SessionありCORS

1. expressで簡単なwebサーバの準備

nodejsのexpressを使用する。
expressでのwebサーバ構築方法は、割愛させていただきます。

npm i -S expressでexpressをインストール
次のapp.jsを用意する。

app.js

const express = require('express');
const app = express();

// "/"にアクセスしたときにres.jsonの引数値を返す
app.get('/', (req, res) => {
  res.json({
    result: 'success',
    message: 'hello express',
  });
});

// 8000ポートで起動
const server = app.listen(8000, () => {
  console.log(`Node.js is listening to PORT: ${server.address().port}`);
});

あとは、node app.js(あるいはnpm scriptsに記述してnpm start)でサーバを起動させる。すると、http://localhost:8000にアクセスしたときにres.json()の引数に指定したjsonを受け取ることができる。

これでまずサーバサイドの実装は終了。

2. 同一オリジンから通信できることの確認

ブラウザを起動させ、urlにhttp://localhost:8000と入力すると、 先程のjsonが画面に表示される。

さらに、fetchメソッドも使って取得できることも確認しよう。
console画面に次を入力して取得できることを確認する。

fetch('/', {
  method: 'GET',
  headers: { 'content-type': 'appliaction/json' }
}).then(r => r.json()).then(r => { console.log(r); })

// コンソール出力値
// {result: "success", message: "hello express"}

3. 別オリジンから通信の失敗確認

グーグルトップページ(https://wwww.google.co.jp)を開いて、同じくコンソール画面に次を入力する

// ドメインが違うので、URLを省略せずに書く
fetch('http://localhost:8000', {
  method: 'GET',
  headers: { 'content-type': 'appliaction/json'}
}).then(r => r.json()).then(r => { console.log(r); })

fetchの第一引数は、http://localhost:8000になる。
"/"は、https://www.google.co.jpを意味するからだ。

その結果は次のとおりである。

f:id:poppon555:20181008204419p:plain

やはり、ドメインが異なるため通信失敗する。 エラー内容にある通り、HTTPリクエストのヘッダーに'Access-Control-Allow-Origin'が存在しないからダメだよと言われるている。(他にもヘッダーに足りない情報がある)

4. expressのCORS設定

異なるオリジンからの通信も許可するために、サーバサイドに変更を加える。

app.js

const express = require('express');
const app = express();

// ------------- ここから --------------
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', "*"); // (1)
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); // (2)
  res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS'); // (3)
  next();
});
// ------------- ここまで --------------

app.get('/', function(req, res) {
  res.json({
    result: 'success',
    message: 'hello express',
  });
});

const server = app.listen(8000, () => {
  console.log(`Node.js is listening to PORT: ${server.address().port}`);
});

追加した内容については、
(1): どんなオリジンからの通信を許可("*"はすべてを意味する)
(2): 指定するheader情報の通信を許可
  今回fetchメソッドでは、headersに'content-type': 'appliaction/json'を指定してるため
(3): GETからOPTIONSまでのHTTPメソッドを使って通信すること許可
ということになる。

これでもういちど、fetchメソッドを実行すると取得できるようになる(やったね)。

5. SessionありCORS(おまけ)

session情報をもたせるときは、もう少し改良が必要になる。 変更箇所のみ記述する

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', req.headers.origin); // (1)
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
  res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Credentials', true); // (2)
  next();
});

(1): "*"ではなく、リクエストからオリジン情報を取得し、そのからの通信を許可
(2): sessionを使うときはこれを追加してあげる
これでsessionありの通信も可能となるが、最後にフロントでのfetchメソッドも確認しておく。

fetch('http://localhost:8000', {
      method: 'GET',
      headers: { 'content-type': 'application/json' },
      credentials: 'include', // (3)
    }).then(r => r.json()).then(r => { console.log(r); })

(3)のとおり、credentailをfetchメソッドのオプションに追加する。

最後に

これでサーバと通信できるフロント開発ができるように助けになってくれれば幸いである。非常に駆け足で説明してきたので、かなり読みにくい文章になってしまった。申し訳ない(あとでもう少し読みやすくしようと思います。)

参考サイト

http://var.blog.jp/archives/60055220.html https://qiita.com/MuuKojima/items/2b2e7bc0db8d5e97ada9 https://blog.kazu69.net/2017/03/23/http-request-using-cors/

Higher Order Componentについて調査

はじめに

Higher Order Componentについて学習。
atomic Designと呼ばれるものを実現するよい手法なのだろうと感じ、
使い方を少し学ぼうと思ったので、その記録を。。。

ゴール

  • MyButtonを拡張して、MyCustomButtonコンポーネントをつくる。
    ポイントは、styleをどうやって拡張させることができるのかということである。

MyButtonについて

MyButtonは、赤色の単純なボタンコンポーネントである。

MyButton.jsx

// @flow
import * as React from 'react';

type PropsType = {
  children: React.Element<any>,
  style: Object,
};

// デフォルトstyle情報
const defaultStyle = {
  backgroundColor: 'red',
};

export default function MyButton(props: PropsType): React.Node {
  const { children, style } = props;

  // デフォルトstyleとpropsで指定されたstyle情報を結合
  const Style = {
    ...defaultStyle,
    ...style,
  };

  return (
    <button
      type="button"
      style={Style} 
      {...props}  
    >
      {children}
    </button>
  );
}

defaultスタイルとpropsから渡ってきたsytleを結合するのがポイント。

MyCustomButton

MyCustomButtonは、MyButtonの特徴を受け継ぎながら独自のスタイルをもったボタン。 背景色が青色になり、フォントサイズや文字色のstyle情報が新たに追加される。

MyCustomButton.jsx

// @flow
import * as React from 'react';
import MyButton from './MyButton';

// Enhance関数は、Componentを引数にとり、コンポーネントを返す関数を返す
function Enhance(Component): (Object) => React.Node {
  return props => (
    <Component
      {...props}
      style={{
        backgroundColor: 'blue',
        fontSize: '50px',
        color: 'white',
      }}
    />
  );
}

// HOC
const MyCustomButton = Enhance(MyButton);

export default MyCustomButton;

Enhance関数のように、関数を返す関数は、高階関数(Higher Order function)と呼ばれる。
このHigher OrderであるEnhance関数によりできた新たなコンポーネントこそ
Higer Order Componentということになる。

childrenがpropsに存在するが、HTML要素にchildren属性はつかなく、またMyCustomButton内では、propsをそのまま伝達するだけでよいので感心する。

App.js

ReactDOM.render(
  <div>
    <MyButton>PUSH!!</MyButton>
    <MyCustomButton 
      onClick={() => {console.log('CUSTOM')}} 
    >
      CUSTOM
    </MyCustomButton>
  </div>,
  document.getElementById('root'),
);

各ボタンの表示結果は以下の通り。 MyCustomButtonで追加されているonClickも問題なく動作する。

f:id:poppon555:20180917222359p:plain

気になること

class属性が付与されると、どちらのスタイルが優先されるのだろうか? これについても調べていこう。