Make it to make it

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

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によるコンパイル・バンドル環境が整った。

VueのInput周りの用法

チェックボックスラジオボタン、複数セレクト、テキスト入力など。

inputs.vue

<template>
  <div id="app">
    <h4>Check Boxes</h4>
    <input type="checkbox" id="checkbox" v-model="checked" />
    <label for="checkbox">This box is {{ checked ? 'checked' : 'unchecked' }}</label>
    <hr />

    <h4>Radio Buttons</h4>
    <div v-for="(dino, index) in dinos" :key="index">
      <label>
        <input type="radio" :value="dino" v-model="chosenDino" />
        {{ dino }}
      </label>
      <br />
    </div>
    <span>Favorite: {{ chosenDino }}</span>
    <hr />

    <h4>Multi Select:</h4>
    <select v-model="selected" multiple>
      <option
        v-for="(period, index) in periods"
        :value="period.value"
        :key="index"
      >{{ period.name }}</option>
    </select>
    <br />
    <span>Selected IDs: {{ selected }}</span>
    <hr />

    <h4>Text Input:</h4>
    <input type="text" v-model="single" />
    <p>{{ single }}</p>
    <hr />

    <h4>Multiline message:</h4>
    <textarea v-model="message" placeholder="add multiple lines"></textarea>
    <p style="white-space: pre">{{ message }}</p>
  </div>
</template>

<script>
import "./dark.min.css";

export default {
  data() {
    return {
      checked: false,
      selected: [],
      chosenDino: "",
      single: "",
      message: "",
      dinos: ["Triceratops", "Velociraptor", "Tyrannosaurus"],
      periods: [
        { name: "Triassic", value: 1 },
        { name: "Jurassic", value: 2 },
        { name: "Cretaceous", value: 3 }
      ]
    };
  },
  methods: {
    addDinos: function() {
      this.count += this.amount;
    }
  }
};
</script>