読者です 読者をやめる 読者になる 読者になる

【React】親コンポーネント更新による子コンポーネントの更新処理を減らす

Reactでは親コンポーネントが更新されると、その子コンポーネント全てに更新処理が走るのですが、値に変更がない子コンポーネントは更新処理を行わないようにできないものかと調べたら、良さそうな方法が見つかったので備忘録として残します。

参考:

ReactでshouldComponentUpdateを使ったチューニングの効果と注意どころ - Qiita Reactの再レンダリングをなるべく減らす - Aqutras Members' Blog

Reactコンポーネントの更新処理をキャンセルするためには、shouldComponentUpdate内でfalseを返してあげれば良い。(デフォルトではtrue) なので、子コンポーネントpropsstateに変更がなければ、shouldComponentUpdate内でfalseを返す処理を実装すれば、データの更新がない子コンポーネントの更新を行わないようにできる。

propsstateが全て値型か、参照型を含むかで異なる実装を行う。

全て値型の場合

react-addons-pure-render-mixinを使うと簡単に実装できて良い。

import PureRenderMixin from 'react-addons-pure-render-mixin';

class HogeComponent extends React.Component {
  constructor(props) {
    super(props);
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }

  render() {
    return (
      ...
    )
  }
}

値型だけでなく参照型を含む場合

lodashの _.isEqual 関数を用いてオブジェクトを比較する。 もちろん値型も見てくれるので、react-addons-pure-render-mixinを使わずに、この方法に統一しても良さそう。

import isEqual from 'lodash/isEqual';

class HogeComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    const propsDiff = isEqual(nextProps, this.props);
    const stateDiff = isEqual(nextState, this.state);
    return !(propsDiff && stateDiff);
  }

  render() {
    return (
      ...
    )
  }
}

とりあえず今の自分の環境では↑の2通りの方法を使って実装しています。

GulpとBabelでES2015をES5に変換する環境をつくる

前回の記事でES2015の文法について覚えたので、今回はES2015をES5に変換するための環境をつくります。

参考:

Node.js

Node.jsとは

サーバーサイドのJavaScript、非同期の処理が得意。

インストール

方法は色々とありますが、今回はnodebrewというバージョン管理ツールを利用してインストールします。 ターミナルを開いて以下のコマンドを実行。 nodebrewのインストールが開始されます。

$ curl -L git.io/nodebrew | perl - setup
========================================
Export a path to nodebrew:

export PATH=$HOME/.nodebrew/current/bin:$PATH
========================================

インストール後、環境設定ファイルにnodebrewのパスを通してねと言われるので、.bash_profileをviエディタで開き、コードを追記します。

$ vi ~/.bash_profile
export PATH=$HOME/.nodebrew/current/bin:$PATH

sourceコマンドで.bash_profileをリロードします。

$ source ~/.bash_profile

nodebrewがインストールされているか確認。

$ nodebrew help
nodebrew 0.9.5

Usage:
    nodebrew help                         Show this message
...

nodebrewをインストールし終わったら、Node.jsの使用するバージョンを指定します。今回は安定版を指定しました。 -vコマンドでバージョンを確認してNode.jsのインストール完了です。

$ nodebrew use stable
use v5.10.1
$ node -v
v5.10.1

npmをインストール

Node.jsで書かれたツールを管理するnpmというパッケージ管理ツールをインストールします。Rubyでいうとgemのようなやつです。 プロジェクト用のディレクトリを作成し、ターミナルでその中へ移動します。今回はpractice/es2015devというディレクトリを作成しました。

$ cd practice/es2015dev

移動したら以下のコマンドでpackage.jsonを作成します。 対話形式で任意の項目を入力します。今回は全てEnterキーを押しました。

$ npm init

...
name: (es2015dev) 
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /Users/XXXX/practice/es2015dev/package.json:

{
  "name": "es2015dev",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this ok? (yes) yes

入力を終えると、ディレクトリ内にpackage.jsonが作成され、中には上の入力内容が記載されています。

Gulp

Gulpとは

様々な作業を自動化できる「タスクランナー」と呼ばれるツールです。Web制作においては、css,js,画像などの圧縮、Sassのコンパイル、ベンダープレフィックスの付与、LiveReloadなどを全て高速に行ってくれます。

インストール

以下のコマンドでGulpをインストールします。-gはグローバル領域にインストールするコマンドです。

$ npm install gulp -g

--save-devコマンドでローカルにもインストールします。

$ npm install gulp --save-dev

するとpackage.jsonにGulpが記載されます。

  "devDependencies": {
    "gulp": "^3.9.1"
  }

Gulpがグローバルとローカル両方に入っているか確認します。

$ gulp -v
[22:01:40] CLI version 3.9.1
[22:01:40] Local version 3.9.1

Babelをインストール

ES2016をES5記法に変換するBabelをローカルにインストールします。

$ npm install babel babel-preset-es2015 gulp-babel --save-dev

.babelrcファイルを作成し、利用するpresetsを記述します。

{
  "presets": ["es2015"]
}

Gulpの設定ファイルをpackage.jsonと同じディレクトリに作成します。 Gulpはバージョン3.9.0からBabelを標準サポートするようになりました。設定ファイルの名前をgulp.jsではなくgulp.babel.jsにすることで、ES2015で設定ファイルを記述できるようになります。

'use strict';

const gulp = require('gulp');
const babel = require('gulp-babel');

// タスクの登録
gulp.task('js', () =>
  gulp.src('src/js/**/*.js') // 読み込むファイル
    .pipe(babel()) // babelを実行
    .pipe(gulp.dest('dist/js')) // 出力先
);

src/jsディレクトリを作成し、その中にES2015で記述したjsファイルを作成します。

const obj = {
  add: (a,b) => a + b
};

準備ができたので、gulpを実行します。

$ gulp js
[22:18:57] Requiring external module babel-register
[22:18:57] Using gulpfile ~/practice/es2015dev/gulpfile.babel.js
[22:18:57] Starting 'js'...
[22:18:57] Finished 'js' after 107 ms

/dist/jsディレクトリにES5変換後のjsファイルが生成されました。

"use strict";

var obj = {
  add: function add(a, b) {
    return a + b;
  }
};

ES2015(ECMAScript2015)の文法を覚える

JavaScriptについての知識は初心者から抜け出せない程度のレベルですが、フロントエンド界隈では現在キャッチアップする価値がある技術の1つとしてES2015が挙げられていたので、勉強したときのメモです。 ES2015の仕様全ては理解しきれていないので、今回は使用頻度の高そうな文法に絞って書いています。

参考:

ES2015とは

ES2015とは、JavaScriptが採用している標準化された言語仕様、ECMAScriptの第6版です。ES2015は2015年6月に発行され、現在(2016年4月)最新バージョンのブラウザでも全ての機能を実装できていないこともあり、Babel等のトランスパイラを用いてES5に変換して使用する方法が一般的です。変換方法については次回の記事で書きます。


letとconst

varのように変数宣言に使用します。上書きリスクが減ることから、今後はletとconstを主に使いそうです。 それぞれの違いは、

  再宣言 再代入 スコープ 巻き上げ
var できる できる 関数スコープ あり
let できない できる ブロックスコープ なし
const できない できない ブロックスコープ なし
var a = 0; // var宣言
let b = 1; // let宣言
const c = 2; // const宣言
// const d; // constは宣言のみはできない

var a = 3; // varは再宣言できる
// let b = 4; // letは再宣言できない
// const c = 5; // constは再宣言できない

a = 6; // varは再代入できる
b = 7; // letは再代入できる
// c = 8; // constは再代入できない

{
  var a = 9; // {}外で宣言したaを上書きする
  let b = 10; // {}内でのみ有効なbを宣言する
  const c = 11; // {}内でのみ有効なcを宣言する

  console.log(a); // 9
  console.log(b); // 10
  console.log(c); // 11
}

console.log(a); // 9
console.log(b); // 7
console.log(c); // 2


function以外の中括弧(ここではif)でもスコープが切れる。

// 今まで
function foo() {
  var bar = 1;
  if (true) {
    var bar = 2;
  }
  console.log(bar);
}
foo(); // 2 ifでスコープが切られないので上書きされる

// ES2015
function foo2() {
  let bar = 1;
  if (true) {
    let bar = 2;
  }
  console.log(bar);
}
foo2(); // 1 ifでスコープが切られるので上書きされない


巻き上げ(宣言よりも前で変数を参照すること)が発生しない。

// 今まで
console.log(e); // undefined 宣言前で変数を参照できてしまう
var e = 'hoge';

// ES2015
// console.log(f); // エラー
// let f = 'hoge';


for内でsetTimeoutが含まれる場合でも正しく動作する。

// 今まで
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 3が3回表示されてしまう
  }, 1000);
}

// ES2015
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 0,1,2 と表示される
  }, 1000);
}


アロー関数

function宣言を() =>という記述に省略できます。

// 今まで
var sum = function(a, b) {
  return a + b;
};

// ES2015
const sum = (a, b) => {
  return a + b;
};

// 関数本体が式だけで表せる場合、{}とreturnを省略できる
const sum = (a, b) => a + b;

// 引数が1つの場合、()を省略できる
const func = a => a * a;


さらに、アロー関数を使うとthisは自動的に補足されるため、setTimeout関数の引数に指定された無名関数の内部でthisがグローバルオブジェクトを指してしまう問題を回避することができます。

// 今まで
var takashi = {
  name: 'Takashi',
  sayHello: function() {
    var self = this; // thisをselfに保存しておく
    setTimeout(function() { // thisはグローバルオブジェクトを指す
      console.log("Hello, I'm " + self.name);
    }, 1000);
  }
}
takashi.sayHello(); // Hello, I'm Takashi

// ES2015
const takashi = {
  name: 'Takashi',
  sayHello() { // functionを省略できる(メソッド定義記法)
    setTimeout(() => { // アロー関数を使うとthisはtakashiを指す
      console.log("Hello, I'm " + this.name);
    }, 1000);
  }
}
takashi.sayHello(); // Hello, I'm Takashi


クラス

これまでJavaScriptにはクラスという概念がなく、prototypeを継承するなどして冗長な記述でクラスを定義していましたが、ES2015でクラスは正式な文法となりました。

// クラスの定義
class Person {
  constructor(name) { // コンストラクタ
   this.name = name; // プロパティ
  }

  sayHello() { // インスタンスメソッド
    console.log("Hello, I'm " + this.name);
  }

  static create(name) { // スタティックメソッド
    return new Person(name);
  }
}

// インスタンスの生成
const saki = new Person('Saki');
saki.sayHello(); // Hello, I'm Saki

// スタティックメソッドの呼び出し
const kota = Person.create('Kota');


クラスの継承はextendsを使い、継承元のコンストラクタメソッドを呼び出すためにはsuperを使います。

// クラスの継承
class Teacher extends Person {
  constructor(name, age) {
    super(name); // 継承元のコンストラクタを呼び出す
    this.age = age;
  }

  sayHello() {
    super.sayHello(); // 継承元のメソッドを呼び出す
    console.log(this.age + ' years old');
  }

  static create(name, age) { // スタティックメソッドの上書き
    return new Person(name, age);
  }
}

const ellen = new Teacher('Ellen', '28');
ellen.sayHello();


引数の拡張

関数に指定する引数のデフォルト値を設定することができるようになりました。

const sum = (a = 1, b = 2) => {
  return a + b;
};
console.log(sum()); // 3
console.log(sum(2)); // 4
console.log(sum(2,3)) // 5


さらに可変長の引数を取ることができるようになりました。(レストパラメータ)

const foo = (a,b,...rest) => {
  console.log('a:', a);
  console.log('b:', b);
  console.log('rest:', rest);
};
foo(1,2,3,4,5);
// a: 1
// b: 2
// rest: [3, 4, 5]


オブジェクト記法の拡張

様々な省略記法が可能になりました。 - オブジェクトのプロパティのキー名と値の変数名が同じ場合の省略記法

var foo = 1;

// 今まで
var obj = {'foo': foo};

// ES2015
const obj = {foo};
  • プロパティのキー名が変数に入った文字列である場合の省略記法
var key = 'foo';

// 今まで
var obj = {};
obj[key] = 1;
console.log(obj.foo); // 1

// ES2015
const obj = {
  [key]: 1
};
console.log(obj.foo); // 1
// 今まで
var obj = {
  add: function(a,b) {
    return a + b;
  }
};
console.log(obj.add(1,2)); // 3

// ES2015
const obj = {
  add(a,b) {
   return a + b;
  }
};
console.log(obj.add(1,2)); // 3

// ES2015 アロー関数を使う場合
const obj = {
  add: (a,b) => a + b
};
console.log(obj.add(1,2)); // 3


展開演算子(スプレッドオペレータ)

可変長の引数を配列として受け取るレストパラメータに対して、配列を複数の引数に展開する機能がスプレッドオペレータです。

var arr = [1, 3, 2];

// 今まで
var max = Math.max.apply(null, arr);
console.log(max); // 3

// ES2015
const max2 = Math.max(...arr);
console.log(max2); // 3


分割代入

分割代入は、配列やオブジェクトからデータを抽出できる機能です。

// 配列の分割代入
const week = ['月', '火', '水', '木', '金', '土', '日',];

const [a, b, c, d, e, f, g] = week;
console.log(a, b, c, d, e, f, g); // 月 火 水 木 金 土 日

// 2番目と5番目の値だけを代入
const [,b,,,e] = week;
console.log(b, e); // 火 金

// レストパラメータの活用
const [a, b,...cdefg] = week;
console.log(a, b, cdefg); // 月 火 ["水", "木", "金", "土", "日"]

// 値の交換
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1

// オブジェクトの分割代入
const obj = {
  name: 'たかし',
  age: 15
};

const {name: a, age: b} = obj;
console.log(a, b); // たかし 15

// オブジェクト代入のプロパティ省略記法
const {name, age} = obj;
console.log(name, age); // たかし 15

// デフォルトの値の指定
const {name, age = 15} = {name: 'たかし'};
console.log(name, age); // たかし 15


イテレータ

イテレータとは、集合から要素を繰り返し取り出すためのオブジェクトです。現在の位置を記録しつつ、中の要素へ1つずつアクセスするnextメソッドを持っています。

const arr = [1, 2, 3];
const iter = arr[Symbol.iterator]();

console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: 2, done: false}
console.log(iter.next()); // {value: 3, done: false}
console.log(iter.next()); // {value: undefined, done: true}


for of文

反復処理可能なオブジェクトの値を列挙することができます。for inはプロパティ名を取得するのに対して、for ofはプロパティの値を取得します。

const arr = [3, 4, 5];
arr.foo = 'hoge';
console.log(arr);

for (let a in arr) {
  console.log(a); // 0 1 2 foo
}

for (let b of arr) {
  console.log(b); // 3 4 5
}