Make it to make it

いろいろ作ってアウトプットするブログ

Higher Order Components (HOC)

例えば、複数のコンポーネント間で同じファンクションやロジックを使用している場合、繰り返しが発生していることになりDRYではない。そんなときに使えるのがHigher Order Components (HOC)である。 実はVanillaJSにもHigher Order Functionsが存在していて、普段実装をするときに使っていたりする。

Higher Order Functions

引数にわたすファンクションがcallbackであり、callbackを含むファンクションをHigher Order Functionsと呼ぶ。

下記にTSで実例を示した。

const taxIncluded = (price: number, taxRate: number) => {
  return price * taxRate * 0.01
}

const higherOrderFunction = (x: number, callback: (arg1: number, arg2: number) => number) => {
  return callback(x, 10)
}

const getTaxValue = higherOrderFunction(4500, taxIncluded)

console.log(getTaxValue)  // 450

Higher Order Components (HOC)

ReactでのHOCの場合は引数にわたすのがReact componentであり、返却されるのは新しいReact componentであり、引数にわたしたReact componentに何かしらの機能を付け足して返却できる。

ボタンの種類がいくつか存在し、各ボタンクリック時にそのボタンのタイプをログするような機能をHOCで付与する例を示す。 ちなみに、実装の際はAnt Designを用いた。

App.jsx

import React from 'react'
import { ButtonPrimaryWithClick, ButtonDashedWithClick, ButtonDangerWithClick, ButtonLinkWithClick } from './components/Buttons'
import './App.css'

function App () {
  return (
    <div className='App'>
      <div className='container' style={{ paddingTop: '3rem' }}>
        <ButtonPrimaryWithClick typeValue='primary' />
        <ButtonDashedWithClick typeValue='dashed' />
        <ButtonDangerWithClick typeValue='danger' />
        <ButtonLinkWithClick typeValue='link' />
      </div>
    </div>
  )
}

export default App

WithClickHoc.jsx

import React from 'react'

function withClick (Component, propKey, propName) {
  return class WithClick extends React.Component {
    constructor (props) {
      super(props)
      this.state = {
        [propName]: this.props[propName]
      }
    }

    handleClick = () => {
      console.log(`You clicked ${propKey} ${this.state[propName]}!`)
    }

    render () {
      const props = {
        [propName]: this.state[propName]
      }

      return (
        <div onClick={this.handleClick}>
          <Component {...props} />
        </div>
      )
    }
  }
}

export default withClick

Buttons.jsx

import React from 'react'
import PropTypes from 'prop-types'

import { Button } from 'antd'
import withClick from './WithClickHoc'

const ButtonPrimary = ({ typeValue }) => <Button type='primary'>Primary</Button>
const ButtonDashed = ({ typeValue }) => <Button type='dashed'>Dashed</Button>
const ButtonDanger = ({ typeValue }) => <Button type='danger'>Danger</Button>
const ButtonLink = ({ typeValue }) => <Button type='link'>Link</Button>

ButtonPrimary.propTypes = {
  typeValue: PropTypes.string
}
ButtonDashed.propTypes = {
  typeValue: PropTypes.string
}
ButtonDanger.propTypes = {
  typeValue: PropTypes.string
}
ButtonLink.propTypes = {
  typeValue: PropTypes.string
}

export const ButtonPrimaryWithClick = withClick(ButtonPrimary, 'button', 'typeValue')
export const ButtonDashedWithClick = withClick(ButtonDashed, 'button', 'typeValue')
export const ButtonDangerWithClick = withClick(ButtonDanger, 'button', 'typeValue')
export const ButtonLinkWithClick = withClick(ButtonLink, 'button', 'typeValue')

Flowによるreactの静的型付け

今までPropTypesやTypeScriptによる静的型付けを述べてきたが、今回はFlowを用いた実装をしてみる。

flow.org

次のようなディレクトリ構造で、5つ星評価のUI実装を考える。

.
├── Stars.css
└── Stars.js

インストール

インストール方法は公式ドキュメントに丁寧に書いてあるので割愛。

セットアップ

npm install --save-dev flow-bin

package.jsonに次のnpm scriptsを追加。

  "scripts": {
    "flow": "flow"
  }

まず最初にnpm run flow initを実行して.flowconfigを生成する。

それ以降は適宜npm run flowを実行しながら開発を進めていく。他のnpm scriptsと合わせて実行したり、nodemonなどと組み合わせたりすると便利そう。

実装

font-awesomeのアイコンを使いたいので、react-iconsをインストールする。

npm install react-icons

Stars.css

.stars {
  display: inline-flex;
}

.stars .star:not(:first-of-type) {
  margin-left: 3px;
}

.star {
  background-color: transparent;
  border: none;
  cursor: pointer;
  outline: none;
  padding: 0;
  appearance: none;
}

Stars.js

// @flow
import * as React from 'react';
import { FaStar } from 'react-icons/fa';
import './Stars.css';

type Props = {
  rating?: number,
};

type State = {
  rating: number,
  colorArray: Array<string>,
};

class Stars extends React.Component<Props, State> {
  constructor(props) {
    super(props);

    this.defaultColor = '#d7d7d7';
    this.selectedColor = '#55c500';

    this.state = {
      rating: this.props.rating,
      colorArray: Array(5)
        .fill()
        .map(() => this.defaultColor),
    };
  }

  componentDidMount() {
    this.updateColor(this.state.rating - 1);
  }

  updateColor = index => {
    const { colorArray } = this.state;

    for (let i = 0; i <= index; i += 1) {
      colorArray[i] = this.selectedColor;
    }
    for (let j = index + 1; j < 5; j += 1) {
      colorArray[j] = this.defaultColor;
    }

    this.setState({
      colorArray,
    });
  };

  render() {
    const { colorArray } = this.state;

    return (
      <div className="stars">
        {colorArray.map((c, i) => (
          <button
            type="button"
            className="star"
            onClick={() => this.updateColor(i)}
            key={i}
          >
            <FaStar color={c} size={24} />
          </button>
        ))}
      </div>
    );
  }
}

Stars.defaultProps = {
  rating: 3,
};

export default Stars;

@flowと先頭に記述するファイルが、型バリデーションの対象となる。

VSCodeを使用している場合、下記のエクステンションを入れれば、fccで一発展開してくれる。 (Class component with flow types skeleton)

marketplace.visualstudio.com

type Propsでpropsのアノテーションtype Stateでstateのアノテーションができる。defaultPropsなどと組み合わせて適宜使う。

使えるアノテーション一覧はこちらを参照のこと。

TypeScriptよりは簡単に実装できるが、TSのようにType Inferenceが効いていないのではないかと思われる。

あとは、とにかくドキュメントが簡潔でわかりやすいところがよい。

後書き

軽い実装の練習がてらFlowを試してみたが、今の世の中のトレンドとしては圧倒的にTypeScriptなので、学ぶならやっぱりTypeScriptの方がベター。

Controlled vs Uncontrolled componentsとrefの使い方

Reactでは、状態管理がDOMではなくReact componentで行われる。 Reactなしである場合、例えばformのinput要素の状態を取ってくる場合は従来はDOM操作によって取ってくるという手法だった。

ではどちらの方法がベターであるのか? この質問に付随して出てくるのがControlled componentsとUncontrolled componentsである。

Controlled components

Controlled componentsでは、状態管理をReactのスタイルに沿って行う。 formやinputの状態は全て属するコンポーネントのstateで管理されるので、各値を更新する場合はstateを更新することになる。

Uncontrolled components

Uncontrolled componentsでは、コンポーネントによる状態管理はなく、コンポーネントの状態は各inputで各々持つ。 つまりDOM内に状態が存在するので、更新するにはDOM操作を行う必要がある。

refを使用したinputのDOMでの状態管理

Controlled

Form.js

class Form extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      email: ''
    }

    this.handleChange = this.handleChange.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
  }
  handleChange(e) {
    this.setState({
      email: e.target.value
    })
  }
  handleSubmit() {
    alert('The email is ' + this.state.email)
  }
  render() {
    return (
      <div>
        <pre>The email is {this.state.email}</pre>
        <br />
        <input
          type='text'
          placeholder='Email'
          value={this.state.email}
          onChange={this.handleChange}
        />
        <button onClick={this.handleSubmit}>Submit</button>
      </div>
    )
  }
}

こちらの例の場合は、状態はコンポーネントのstateが持っていることになる。 このコードを、Uncontrolled componentsにしてDOMでの状態管理ができるようにしてみる。

Uncontrolled

Form.js

class Form extends React.Component {
  constructor(props) {
    super(props)

    this.input = React.createRef('')
    this.handleSubmit = this.handleSubmit.bind(this)
  }
  handleSubmit() {
    alert('The email is ' + this.input.current.value)
  }
  render() {
    return (
      <div>
        <input
          type='text'
          placeholder='Email'
          ref={this.input}
        />
        <button onClick={this.handleSubmit}>Submit</button>
      </div>
    )
  }
}

refのpropをinputフィールドに持たせ、値を取得する際にはthis.input.current.valueで持ってくることができる。

どちらを使用すべきか?

結論からいうとControlled componentsで書いた方がよい。 Reactに状態管理を委ねる方が、変更にたいしてリアクティブなUIを作ることができるから。

PropTypesによるバリデーション

JavaScriptにはBoolean, Null, Undefined, Number, String, Symbol, Objectの7種類のデータ・タイプが存在する。 それらの型をハンドリングする際にFlowとかTypeScriptなどのツールが導入されているわけだが、Reactのプロジェクトでそこまで導入するのが面倒で手間なとき、PropTypesによるバリデーションが便利。 reactjs.org

以下にPropTypesの使用例を示す。

import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';

class Badge extends React.Component {
  render() {
    const { authed, style, name, handle, img, addFriend } = this.props;

    if (authed !== true) {
      return <p>You need to log in.</p>;
    }

    return (
      <div style={style}>
        <img
          src={img}
          style={{ borderRadius: '50%' }}
          alt=""
          width="200"
          height="200"
        />
        <h1 style={{ margin: '15px 0 5px', fontSize: '2rem' }}>{name}</h1>
        <h3 style={{ margin: '5px 0', fontSize: '1rem' }}>@{handle}</h3>
        <button
          type="button"
          onClick={addFriend}
          style={{ margin: '10px 0 0' }}
        >
          Add Friend
        </button>
      </div>
    );
  }
}

Badge.propTypes = {
  authed: PropTypes.bool,
  style: PropTypes.objectOf(
    PropTypes.oneOfType([PropTypes.number, PropTypes.string])
  ),
  name: PropTypes.string.isRequired,
  handle(props, propName, component) {
    if (props[propName] === '' || !/^([^\s]|\w+)$/.test(props[propName])) {
      return new Error(`Invalid prop passed to ${component}`);
    }
  },
  img(props, propName, component) {
    if (
      props[propName] === '' ||
      // eslint-disable-next-line no-useless-escape
      !/^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/.test(
        props[propName]
      )
    ) {
      return new Error(`Invalid prop passed to ${component}`);
    }
  },
  addFriend: PropTypes.func,
};

ReactDOM.render(
  <Badge
    name="Random Dude"
    handle="RandomDude"
    img="https://i.pravatar.cc/600"
    authed
    style={{
      width: 300,
      margin: '0 auto',
      border: '3px solid #000',
      padding: 20,
      borderRadius: 3,
      display: 'flex',
      flexDirection: 'column',
      alignItems: 'center',
    }}
    addFriend={() => alert('Added!')}
  />,
  document.getElementById('app')
);

PropTypesのビルトインメソッドの他にも、上記のようにregexなどでのCustom validationも可能になっている。 下記のポストも参考になるので目を通しておくとよい。

blog.bitsrc.io

this: new binding, window binding

new binding

/**
 * new Binding
 */
const Animal = function(color, name, type) {
  this.color = color
  this.name = name
  this.type = type
}
const zebra = new Animal('black and white', 'shimauma', 'plant-eating animal')
console.log(zebra)

下記のようにオブジェクトを返却する。

Animal { color: 'black and white', name: 'shimauma', type: 'plant-eating animal' }

window binding

/**
 * window Binding
 */
const sayAge = function() {
  console.log(this.age)
}
const me = {
  age: 25
}
sayAge.call(me)

引数なしで実行した場合

sayAge()

undefinedを返却する

const sayAge = function() {
  'use strict'
  console.log(this.age)
}

strictモードで実行すると、エラーをはく。

Cannot read property 'age' of undefined

this: Implicit binding / Explicit binding

Implicit binding

次のようなファクトリーファンクションがあるとする。 いわゆる実行時にオブジェクトを返却するファンクション。

/**
 * Implicit Binding
 *     Left of the dot at call time
 */
const Person = function(name, age) {
  return {
    name,
    age,
    sayName() {
      console.log(this.name)
    },
    mother: {
      name: 'Tracy',
      sayName() {
        console.log(this.name)
      }
    }
  }
}
const john = Person('John', 49)

john.sayName()  // John
john.mother.sayName()  // Tracy

この場合、JohnとTracyがコンソールログされることになる。 ここでのthisは直上のプロパティを示す。

Explicit binding

よく混同しがちなcall, apply, bindを交えながら。 以下のようなファンクションとオブジェクト、配列を定義する。

/**
 * Explicit Binding
 *     call, apply, bind
 */
const sayName = function (lang) {
  console.log(`My name is ${this.name}, and I know ${lang.join(', ')}!`)
}
const sayName2 = function (lang) {
  console.log(`My name is ${this.name}, and I know ${lang}!`)
}
const stacey = {
  name: 'Stacey',
  age: 30,
}
const languages = ['JavaScript', 'Ruby', 'Python']

callメソッド

引数をそのまま一つずつ渡す。

// `call`: Pass arguments one by one
sayName.call(stacey, languages)

実行結果

My name is Stacey, and I know JavaScript, Ruby, Python! 

applyメソッド

引数を配列のアイテムとして渡す。下記の例の場合だと1番目が返却される。

// `apply`: Pass arguments as an array
sayName2.apply(stacey, languages)

実行結果

My name is Stacey, and I know JavaScript! 

bindメソッド

callメソッドと同じだが、こちらはファンクションを返す。

// `bind`: Same as `call` but returns a new function
const newSayName = sayName.bind(stacey, languages)
newSayName()

実行結果

My name is Stacey, and I know JavaScript, Ruby, Python! 

create-react-appに頼らずにReact環境構築

webpack + babelでの開発環境に加えて、eslint + prettierの設定まで。

パッケージのインストール

react + react-dom

npm install react react-dom

ちなみにreact-domreactをDOM向けにコンパイルするパッケージで、 DOM以外の環境などにコンパイルする際には別のパッケージなどを使う。 例えば、スマホアプリ向けにはreact-nativeを使う。

webpack + babel

webpackで必要なloaderとpluginも最低限に追加する。

loader

babel-loader, style-loader, css-loader

plugin

html-webpack-plugin

npm install --save-dev @babel/core @babel/preset-env @babel/preset-react webpack webpack-cli webpack-dev-server babel-loader css-loader style-loader html-webpack-plugin

eslint + prettier

最近よく使っているWes Bosのセットアップ github.com

こちらのGithubリポジトリに書いてある通りにセッティングを進める。

ESLintのルールをオーバーライドする場合は.eslintrcにオーバーライドするルールを加える。

.eslintrc

{
  "extends": [
    "wesbos"
  ],
  "rules": {
    "no-console": 2,
    "prettier/prettier": [
      "error",
      {
        "trailingComma": "es5",
        "singleQuote": true,
        "printWidth": 120,
        "tabWidth": 8,
      }
    ]
  }
}

.eslintignore

Webpackを通した後に生成されたdist/フォルダ内などはlint対象ではないので、 .eslintignoreを作成して指定しておく。

dist/

ディレクトリ構造

.
├── package-lock.json
├── package.json
├── src
│   ├── index.css
│   ├── index.html
│   └── index.js
└── webpack.config.js

webpack.config.jsの設定

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.(js)$/,
        use: 'babel-loader',
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  mode: 'development',
  plugins: [
    new HtmlWebpackPlugin({
      template: 'src/index.html',
    }),
  ],
};

これで最低限のReact開発環境が出来上がる。

エントリーポイントである./src/index.jsを通じてバンドルを行い、index.htmlの方にアウトプットする。

Webpackの各loaderによって、下から順番に(配列の場合後ろから前に)処理が進む。 まずcss-loaderによってエントリーポイントでimportされたcssがバンドルされ、次にstyle-loaderでlinkタグへの展開が行われる。

最後にbabel-loaderを通すことによって、ブラウザが認識可能なjsにコンパイルが行われる。 もちろん、ここではReact向けのbabelの設定を用意しておかなければならない。

.babelrc

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

pluginsに記述したhtml-webpack-pluginにより、 htmlファイルにエントリーポイントのJSファイルの読み込み記述が追加され、 htmlファイルも一緒にdist/にコピーされる。

package.jsonに必要なnpm scriptsを追記しておく。

{
  "name": "hoge",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.js",
  "scripts": {
    "dev": "webpack-dev-server",
    "build": "webpack",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "clean": "rimraf dist/"
  }
}

これでReactのwebpack-dev-serverによるHot module replacementと webpackによるコンパイル・バンドル環境が整った。