Flowによるreactの静的型付け
今までPropTypesやTypeScriptによる静的型付けを述べてきたが、今回はFlowを用いた実装をしてみる。
次のようなディレクトリ構造で、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)
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も可能になっている。
下記のポストも参考になるので目を通しておくとよい。
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-dom
はreact
を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>