20200525のReactに関する記事は11件です。

[React][Reactチュートリアル]ふりがな

はじめに

React.jsを知るためにReactチュートリアルを使用しました。
その際に書いたコードに自分なりのふりがなをつけたため、記事として残したいと思います。

チュートリアル:React の導入

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

function Square(props) {
  return (
    // 盤面に'X'または'Y'を表示させる
    <button
      className="square"
      onClick={props.onClick}>
      {props.value}
    </button>
  )
}

class Board extends React.Component {
  renderSquare(i) {
    return (
      // マス目の番号である1~9の数字が渡されてくる
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
      />
    );
  }

  render() {
    return (
      // 各マス目にを番号を付与する
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}



class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      // historyに変化していく盤面の状態を都度保存する
      history: [{
        // 最初の盤面の状態として3x3の計9マスに初期値として、それぞれにnullを保存する
        squares: Array(9).fill(null),
      }],
      // stepNumberで今が何手目なのかを保存していく
      stepNumber: 0,
      // プレイやーを判断する
      // xIsNext = trueの時は'X', falseの時は'O',
      xIsNext: true,
    };
  }

  handleClick(i) {
    // 過去の履歴に戻った際にstepNumber + 1以降のデータを削除して更新
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    // 勝負がついている場合、returnによりhandleClickを終了させ入力をさせない
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      // 元のhistoryを更新しないためにconcatを使用
      history: history.concat([{
        squares: squares,
      }]),
      // 現在の手数で更新
      stepNumber: history.length,
      // !this.state.xIsNextで毎回trueとfalseを反転させて更新
      xIsNext: !this.state.xIsNext,
    });
  }

  jumpTo(step) {
    this.setState({
      // stepNumberに選択された履歴番号(何手目か)で更新
      stepNumber: step,
      // 選択された履歴のプレイヤーを判定する
      xIsNext: (this % 2) === 0,
    });
  }

  render() {
    const history = this.state.history;
    // stepNumberを基準に盤面の現状を更新
    const current = history[this.state.stepNumber];
    // calculateWinnerに現状の盤面情報を渡し勝者情報をwinnerに代入
    const winner = calculateWinner(current.squares);

    // 履歴に戻るボタンを履歴数分作成し、movesに代入
    const moves = history.map((step, move) => {
      // 履歴の有無で条件分岐してテキストを代入
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        // この配列(履歴)は途中で要素の変更などがされないためkeyにはインデックス番号を使用します
        <li key={move}>
          {/* 履歴に戻るボタンを作成
          押されるとjumpTo関数をコールし、戻る履歴の番号を渡す */}
          <button onClick={() => this.jumpTo(move)}>
            {desc}
          </button>
        </li>
      );
    });

    // ゲーム状況に応じてstatusにアナウンスする文を代入する
    let status;
    if (winner) {
      // 勝者がいる場合(ゲームが終了した場合)
      status = 'Winner: ' + winner;
    } else {
      // 勝者がいない場合(ゲームが継続する場合)
      // xIsNextの真偽知によって次のプレーヤー('X' または 'O')を表示する
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          {/* 作成した過去の履歴に戻るボタンを表示 */}
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);

// 勝ち判定メソッド
function calculateWinner(squares) {
  // 全ての勝ちパターンを配列代入する
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  // 勝ちパターンの数だけ繰り返す
  for (let i = 0; i < lines.length; i++) {
    // linesから取り出した要素を、さらに1つずつa,b,cに代入する
    const [a, b, c] = lines[i];
    // 現在の盤面が勝ちパターンと一致しているかを判定
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      // 一致していれば勝者('X' または 'O')を返す
      return squares[a];
    }
  }
  // 勝者がいなければnullを返す
  return null;
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2. State 管理を Redux に移行する - Redux よくわからんので Todo つくる

はじめに

前回は、React のみでこのようななんでもない Todo アプリを作成しました
react-todo.gif

はい
なんでもないですね

今回は当初の目的である、State 管理の Redux への移行を行います

今回からいろいろと Rollup に惨敗して Webpack 使ってます
なんにも知らないくせに使うから...

前回のコードに変更はないので、それだけ

なぜ Redux を使うのか?

「なぜ State 管理を分離するのか?」と言ったほうが正しいかも

今回のような、小規模も小規模なアプリではほぼ無縁な問題ですが、アプリの規模が大きくなると、以下のような様々な問題が浮き彫りとなりやがります

  • State 分散されすぎ... いちいち別ファイル開くのが面倒、てかどこ? ?
    • (トップコンポーネントに State 集中しすぎることのほうが多そう
  • State 管理用のコードがコンポーネント内に増えまくり... お手上げ ?‍♂️
  • UI、State 処理のコードが混在して、コードが読みづらい... ?
  • Prop のバケツリレーつらい ?
  • etc...

しかし、Redux を使うと...?

  • State は一点管理✨ どこにあるかも一目瞭然!! ?
  • State 管理のコードはコンポーネント外部に追いやり! State 管理は Redux に任せとき~~ ?
  • コンポーネントは描画に集中! 内部には UI 処理のコードだけ!! ?
  • 必要なコンポーネントが直接 State 変更関数を受け取り!! ?
  • etc...

YEAH ?

React 単体でもある程度マシに出来ると思いますが、そもそも React は UI 構築ライブラリであることを意識しておいたほうが良いんじゃないかなあ~と思います
(結局バケツリレーはどうにもなりませんし)

あくまで React の言う State というのは "UI(表示) に関するもの" のことであって "表示されるデータ" では無い...的な(わかって)
そこをはっきりとさせることで、開発の効率は確実に上がると考えます
(バケツリレーも無くなりますし)

コーディングの基本は分割...ってね

ディレクトリ構造

src/
 ├ components/
 │  └ app/
 ├ actions/
 ├ store/
 │  └ reducer/
 └ index.ts - エントリ

まず、Redux 用のフォルダが追加されるため、前回 src/ 直下にあった App コンポーネントフォルダを components/ に移動しています

そして、Redux 用に actions/, store/, store/reducer/ を作成します

パッケージの追加

yarn add redux react-redux
yarn add -D @types/react-redux

Store の作成

まずは Store を作らないことには React との連携もへったくれも無いので Redux 単体の機能である Store を作成します

必要な要素

Action
コンポーネントが、Store に対して「何が起きたか」を説明する
store.dispatch() を使用して Store に送信する

Action は以下のような type プロパティを持つただのオブジェクト

{
  type: "Reducer が Action の種類を識別するための文字列",
  // あとは自由
  // State 変更に必要なデータを入れておく
}

Action Creator
コンポーネントからデータを受け取り、Action を作成する

Reducer
Store に送信された Action を受け取り、State がどのように変化するかを指定する
事実上実際 State の変更を担当する大事なトコ

Store
アプリケーションに一つだけ存在し、State を保持する

  • State へのアクセス手段 (store.getState())
  • State の更新手段 (store.dispatch())
  • 更新リスナの登録 (store.subscribe())

を提供する

Action Creator

誤字とかでエラーが出るのを防ぐために Action type を別に定義しておきます
Action Creator に渡した時に string 型になるのを防ぐために as const を付けています

src/actions/todo/index.ts
const ADD_TODO = "ADD_TODO" as const;
const TOGGLE_COMPLETED = "TOGGLE_COMPLETED" as const;
const DELETE_TODO = "DELETE_TODO" as const;
src/actions/filter/index.ts
const SET_FILTER = "SET_FILTER" as const;

あとは必要なデータを引数に受け取り、いい感じに加工して Action を作ります
若干 JSON API 意識で、必要なデータは全て data プロパティ内に入れています

src/actions/todo/index.ts
const addTodo = (text: string) => ({
  type: ADD_TODO,
  data: { id: Math.random(), text, complete: false },
});

const toggleCompleted = (id: number, isCompleted: boolean) => ({
  type: TOGGLE_COMPLETED,
  data: { id, isCompleted },
});

const deleteTodo = (id: number) => ({
  type: DELETE_TODO,
  data: id,
});
src/actions/filter/index.ts
const setFilter = (filter: FilterStateType) => ({
  type: SET_FILTER,
  data: filter,
});

型定義

Reducer で型チェックするため、ReturnType を使用して、Action の型を定義します

ReturnType<typeof addTodo>
// ⇓
{
  type: "ADD_TODO";
  data: {
    id: number;
    text: string;
    complete: boolean;
  };
}
src/actions/todo/types.ts
type AddTodoAction = ReturnType<typeof addTodo>;
type ToggleCompletedAction = ReturnType<typeof toggleCompleted>;
type DeleteTodoAction = ReturnType<typeof deleteTodo>;

type TodoActions =
  | AddTodoAction
  | ToggleCompletedAction
  | DeleteTodoAction;
src/actions/filter/types.ts
type SetFilterAction = ReturnType<typeof setFilter>;

Reducer

引数の state, action の型定義のため、Redux.Reducer<State, Action> を使用します

初期化時には stateundefined が渡されるため、初期値を設定し
Action type で識別し、新しい State を返します

default case では引数 action を never 型に割り当てることで、絞り込みの漏れが無いようにしています
参考: TypeScript 2.0のneverでTagged union typesの絞込を漏れ無くチェックする

State の生成コードは前回と変わりないですね

src/store/reducer/todo/index.ts
const todoReducer: Redux.Reducer<TodoStateType, TodoActions> = (
  state = new Map(),
  action
) => {
  switch (action.type) {
    case ADD_TODO:
      return new Map(state.set(action.data.id, action.data));

    case TOGGLE_COMPLETED:
      const todo = state.get(action.data.id);

      if (todo) {
        return new Map(
          state.set(todo.id, { ...todo, complete: action.data.isCompleted })
        );
      }
      return state;

    case DELETE_TODO:
      state.delete(action.data);
      return new Map(state);

    default:
      const __check: never = action;
      return state;
  }
};

Filter の Action は 1種類だけなので if で

src/store/reducer/filter/index.ts
const filterReducer: Redux.Reducer<FilterStateType, SetFilterAction> = (
  state = "ALL",
  action
) => {
  if (action.type === SET_FILTER) return action.data;

  return state;
};

分割された Reducer を combineReducers を使用して1つにまとめます

src/store/reducer/index.ts
combineReducers({ todoReducer, filterReducer });

Store

Reducer を createStore に渡せば Store の完成です

src/store/index.ts
createStore(reducer);

型定義

前回の State 型と
Store 全体の型を定義します

src/store/types.ts
type TodoType = { readonly id: number; text: string; complete: boolean };
type TodoStateType = Map<number, TodoType>;

type FilterStateType = "ALL" | "COMPLETED" | "ACTIVE";

type StoreType = {
  todo: TodoStateType;
  filter: FilterStateType;
};

React との連携

私が React 触り始めるより前の話でしたが、hooks に対応したため、面倒な stateToProps だとか dispatchToProps なんかは書かずに、呆れるほど簡単に連携可能になりました

ここが一番面倒で厄介でよくわからん意味不明な所だったのでもうこれは革命です
Redux 全然よくわからんくないです、タイトル詐欺です
(一回 connect を使って書いちゃった後に気づいたのはひみつ)

まずはトップコンポーネントを Provider でラップし、store を渡します
これで、ネストされたコンポーネントで Redux にアクセス出来るようになります

src/components/app/index.tsx
import { Provider } from "react-redux";
...
const App: React.FC = () => {
  return (
    <Provider store={store}>
      <AddTodo />
      <ToggleFilter />
      <TodoList />
    </Provider>
  );
};

そうすれば後は以下の hooks で実際に Redux にアクセスし、State の取得、Dispatch を行うだけです

useSelector()

stateToProps に当たる機能です

引数として、selector 関数を受け取り、store の値を返します
selector 関数は引数に store を受け取り、値を返す関数です

TodoList での例

src/components/app/todo-list/index.ts
import { useSelector } from "react-redux";
...
const TodoList: React.FC = () => {
  // Todo のリストを取得
  const todoList = useSelector((store: StoreType) => store.todo);
  // 現在のフィルタを取得
  const filter = useSelector((store: StoreType) => store.filter);
  ...

useDispatch()

dispatchToProps に当たる機能です

store.dispatch() を返します

Todo での例
特定の Action のみ受け付けるよう型で縛ってます

src/components/app/todo-list/todo/index.ts
import { Dispatch } from "redux";
import { useDispatch } from "react-redux";
...
const Todo: React.FC<Props> = ({ todo }) => {
  const dispatch = useDispatch<Dispatch<ToggleCompletedAction | DeleteTodoAction>>();
  ...

まとめ

私の言う「Redux よくわからん」は十中八九 connect 周りの事だったので、hooks が使えたことで、大体 Prop で受け取っていた所を、useSelector, useDispatch に変更した感じになっちゃいました
記事書く前は使えるとか知らなかったんだもん...

それはそれとして、ディレクトリ構造とか型定義の場所とかを自分的に整理することができたので良かったです

趣旨ズレですが、Redux を使う利点もよく分かったと思います
TodoList とかが特に分かりやすい: Only React / With Redux

今回作成したコードは こちら (canoypa/react-redux-test-todo-app) にあります

Prev: とりあえず React だけで Todo
React (State) -> Redux の流れを掴むため、という名目で記事稼ぎのため一旦 React のみで Todo アプリをつくってます

参考

Redux入門【ダイジェスト版】10分で理解するReduxの基礎 、及びもと記事
Redux Docs
React Redux Docs

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React vs Vue.js vs Angular.js 【データバインディング編】

この内容について

この内容は、私が運営しているサイトの一部抜粋です。よければそちらもご活用ください。
Reactチートシート | コレワカ
Vue.jsチートシート | コレワカ
AngularJSチートシート | コレワカ

それぞれの特徴

React Vue.js AngularJS
特徴   状態管理に特化したUI構築のためのライブラリ トランスコンパイル不要なUI構築のためのライブラリ 大抵の機能が全て揃うフルスタックなフレームワーク
開発規模 小規模〜大規模 小規模〜中規模 中規模〜大規模
組み合わせ   Redux・TypeScript・webpack・babelなど Vuex・Laravel・Firebaseなど TypeScript・AWSなど

データバインディングとは

データと描画を同期する仕組みのこと

それぞれのコード

React

See the Pen React_onChange by engineerhikaru (@engineerhikaru) on CodePen.

Vue.js

See the Pen Vue.js_v-model by engineerhikaru (@engineerhikaru) on CodePen.

AngularJS

See the Pen AngularJS_ng-model by engineerhikaru (@engineerhikaru) on CodePen.

簡単な解説

React

Reactは、単方向データバインディングなので、
setStateでデータを保管し、changeイベンド(onChange)で、View ⇆ Modelを実現しています。

Vue.js

Vue.jsは、双方向データバインディングなので、
Model関数(v-model)を使って、View ⇆ Modelを実現しています。

AngularJS

AngularJSは、双方向データバインディングなので、
Model関数(ng-model)を使って、View ⇆ Modelを実現しています。

おまけ

jQueryで書いた場合

キーが押された時に処理を実行するkeyup関数とテキスト出力をするためのtext関数を使って、
View ⇆ Modelを実現しています。


See the Pen
jQuery_databinding
by engineerhikaru (@engineerhikaru)
on CodePen.


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6+Reactで付箋アプリっぽいページを作ってみた。5(react-contenteditable導入編)

記事について

前回まででUIをそれっぽくしてみましたが、そろそろ付箋の中身を書き換えたくなってきました。
ということで、やってみます。

関連する記事

今までの記事です。
その1(環境構築〜モデル作成編)
その2(API作成編)
その3(UI作成編1)
その4(UI作成編2)
その5(react-contenteditable導入編)
おまけ(モデルのテスト編)

divのまま編集してみたい。

またしても手段を目的として面倒臭いことになるパターンです。
これまで、タスクのタイトルや説明などをdivタグで作ってきてしまったので、そのまま編集可能にできたら修正が少なくなって楽かもしれない?

divタグを編集可能にするにはcontentEditable=trueという属性を使います。
参考(MDN Web Docs - contentEditable)

なんだ、簡単そうだ。
早速、以前作ったSticky.jsのタイトル表示部分で試してみます。

app/javascript/components/Sticky.js(変更箇所のみ)
// contentEditable="true"を追加
<div className="TaskTitle" contentEditable="true">{this.props.task.title}</div>

実際にブラウザで表示するとどうなるか!?

おやおや???
スクリーンショット 2020-05-23 20.26.49.png

Reactから「contentEditableはあなたの責任で使いなさいよ」という警告メッセージが出ていました。

はーい、がんばりまーす。
って言うほど頑張れないので、メッセージ出さない方法ないかなぁ?
と調べた結果、react-contenteditableというものを見つけたので、使ってみたいと思います。

react-contenteditableの追加

以下のとおり、yarnで追加できました。

shell
bundle exec yarn add react-contenteditable

react-contenteditableを使ってみる。

インストールができたので、ソースコードに追加してみます。
対象は、先ほどのSticky.jsです。

ソースコードの変更

app/javascript/components/Sticky.js
import React from "react"
import PropTypes from "prop-types"

// ContentEditableコンポーネントをimportします。
import ContentEditable from "react-contenteditable"

class Sticky extends React.Component {
  // (中略)
  // レンダラです。
  render () {
    return (
          { /* 先ほどのdivをContentEditableに変更します。 */ }
          { /* 表示内容はhtml=で指定します。 */ }
          <ContentEditable className="TaskTitle" html={this.props.task.title} />
          { /* 以後、色々省略 */ }

    );
  }
}

export default Sticky

コメントしてあるとおり、最初のimport文でContentEditableをインポートし、render()内で編集可能にしたい要素をContentEditableに置き換えます。

なお、propsとして指定できるのは、以下のもののようです。

お名前 ご説明
innerRef refに指定する値(あとで値の取得等に使うためのものです。) Object または Function
html 要素の値です。名前の通りhtmlが利用できます。必須要素です。(null値はエラーになる。。) String
disabled trueにすると編集不可にできます。 Boolean
onChange innerHTMLの内容が変更されたときに呼び出されるハンドラを指定します。 Function
onBlur フォーカスが離れた時のハンドラです。 Function
onKeyUp キーを離した際のハンドラ Function
onKeyDown キーを押した際のハンドラ Function
className そのまんまクラス名です。 String
style スタイルも指定できるんですね。 Object

動作確認

ソースコードを直したら、早速反映されたか確認してみます。
スクリーンショット 2020-05-24 10.33.18.png

おぉ!編集できる!!
ならば、この値を取得して保存する流れを作ってみます。

CotentEditableで作成した要素から値を取得する。

reactで編集可能な要素の値を取得するには、onChangeを使ってstateに値を保存するなどの方法が考えられますが、ここではrefを使った方法を使ってみます。
ContentEditableコンポーネントでは、innerRefというプロパティを使うことで実現します。

具体的には以下のようなソースコードとなります。(変更箇所に1〜5の番号をふってあります。)

app/javascript/components/Sticky.js
import React from "react"
import PropTypes from "prop-types"

// ContentEditableコンポーネントをimportします。
import ContentEditable from "react-contenteditable"

class Sticky extends React.Component {
  // コンストラクタです。
  constructor(props) {
    // おまじないです。
    super(props);

    /*
    ((中略))
    */

    // createRefで要素参照用のインスタンス変数を作ります。 -- 1
    this.taskTitle = React.createRef();

    // 保存ボタンクリック時のハンドラをバインド -- 2
    this.onSaveButtonClick = this.onSaveButtonClick.bind(this);

  }

  /*
   ((さらに中略))
  */

  // 保存ボタンクリックイベントハンドラ -- 3
  onSaveButtonClick(event){
    // 以下のように"current.textContent"で要素の値が取得できます。
    // ここでは、とりあえず取得した値をconsole.logに吐いてみます。
    console.log(this.taskTitle.current.textContent);
  }

  // レンダラです。
  render () {
    return (
          { /* innerRef={this.taskTitle}という記述を追加しました。 -- 4 */ }
          <ContentEditable className="TaskTitle" html={this.props.task.title} innerRef={this.taskTitle} />
          { /* 保存ボタンとして使う要素を追加します。 -- 5 */ }
          <div className="TaskFooter">
            <div className="TaskUpdateButton" onClick={this.onUpdateButtonClick} >save</div>
          </div>
          { /* 以後、色々省略 */ }

    );
  }
}

表示する要素が増えてしまったので、ついでに、スタイルシートも追加しておきます。

app/assets/stylesheets/white_board.scss
// 付箋のフッター
div.TaskFooter {
  grid-row: 8;
  grid-column: 1 / 3;
  display: flex;
  justify-content: space-between;
}

// 保存ボタン
div.TaskUpdateButton {
  color: #0000FF;
  font-weigt: bold;
  font-size: 10px;
  text-align: right;
}

試してみます。
スクリーンショット 2020-05-24 15.13.44.png
タイトルを編集(hogegeってしたり、updatedってしたり)して、"save"を押してみると。
おぉ、ログが出た。
スクリーンショット 2020-05-24 15.04.09.png

実際にDBに保存させてみる。

編集した値が取得できることが分かったので、実際にDBに反映する流れを作ってみます。

シーケンス図で考える。

Railsで作ってるAPIにて

APIの処理イメージ
@task = Task.find(params[:task][:id]);
@task.update(params[:task]);

みたいな感じになれば良いわけですが。
誰がこのAPIを呼ぶ?
みたいなことを考えねばなりません。

で、以前作っているWhiteBoard.jsですでにAPI呼び出しを行っているので、
こいつにまとめてしまおう。
ちょっと複雑になるので、シーケンス図を起こしてみました。
スクリーンショット 2020-05-24 16.28.50.png

図にするとわかりやすいですね。
最終的にWhiteBoardのonTaskSave()が呼ばれるようにpropsを引き継いでいけば実装できそうです。

APIを用意してみる。

コントローラの準備

まずは、コントローラの準備をします。

app/controllers/api/tasks_controller.rb
  # タスク情報更新処理
  def update
    # エラーメッセージリストを初期化
    @errmsgs = [];

    # 無害化したパラメータを取得
    updparam = update_params();

    begin
      # タスク情報を取得
      @task = Task.find(updparam[:id]);

      # アップデート実行
      if ! @task.update(updparam) then
        # エラー時はエラーメッセージリストにエラーメッセージを追加しておいて
        @task.errors.each do | key |
          @task.errors[key].each do | message |
            @errmsgs.push(key.to_s + ":" + message);
          end
        end
        # エラー表示用レンダラーを指定します。
        render :show_error
      end

    rescue => ex
      # こちらも、例外情報をメッセージリストに追加して
      @errmsgs.push(ex.to_s);
      # エラー表示用レンダラーを指定します。
      render :show_error
    end

    # 更新がうまくいったら、json形式で更新結果をお知らせするので、
    # app/views/api/tasks/update.json.jbuilderを用意しておきます。
    # Railsのデフォルト動作では、コントローラと同じ名前のviewを表示しようとするので、"render :update"などと書く必要はありません。
    # 必要はありませんが、知らないと分からないですよね。。

  end

  private
    # update時パラメータの取得
    def update_params()
      return params.require(:task).permit(:id, :title, :description, :due_date, :user_id);
    end

Viewを作る

次にviewを用意します。

app/views/api/tasks/update.json.jbuilder
# 更新後のデータを返せば良いかなぁ。と。
json.task do
  json.id(@task.id);
  json.title(@task.title);
  json.description(@task.description);
  json.due_date(@task.due_date.strftime("%Y-%m-%d"));
  json.user_id(@task.user.id);
end

エラー時はこんな感じでいいかしらね。

app/views/api/tasks/show_error.json.jbuilder
# エラーメッセージをJSON形式で返します。
json.errors @errmsgs do | msg |
  json.message(msg);
end

ルーティングを追加する。

コントローラにアクションを追加したので、ルーティングを追加します。
updateだから、putで。

config/route.rb
namespace :api do
  put 'tasks/update'
end

とりあえずテスト

ブラウザでいちいち動作確認するのめんどいので、テストコードを追加しときます。
エラー系は、とりあえず後で。。

test/controllers/api/tasks_controller_test.rb
  test "should success to update" do
    task2 = tasks(:task2);
    due_date = Date.new(1894,2,11);

    put(api_tasks_update_url(:json), params: { task: { id: task2.id, title: "title-updated", description: "description-updated", due_date: due_date }});

    assert_response :success

    json_data = ActiveSupport::JSON.decode(@response.body);
    assert_equal(task2.id, json_data['task']['id']);
    assert_equal("title-updated", json_data['task']['title']);
    assert_equal("description-updated", json_data['task']['description']);
    assert_equal(due_date, Date.parse(json_data['task']['due_date']));
  end

フロントエンド側に処理を追加していく

Rails側の修正

Reactで作ったコンポーネントから、追加したアクション(今回はtasks/update)を呼び出してもらうため、react_compnent()呼び出し時の引数を修正します。

app/views/white_board/main.html.erb
<%
# update_task_url: api_tasks_update_url(:json)
# を追加しました。
%>
<%= react_component('WhiteBoard', { 
  title: 'You can let others do your task', 
  user_tasks_url: api_users_user_task_list_url(:json), 
  switch_user_url: api_tasks_switch_user_url(:json),
  update_task_url: api_tasks_update_url(:json),
  secure_token: form_authenticity_token
}) %>

WhiteBoard.jsの修正

ここには、updateを呼び出すための処理の追加と、自身の"onTaskSave()"をコールバックしてもらうための処理を追加します。

app/javascript/components/WhiteBoard.js
import React from "react"
import PropTypes from "prop-types"

// 自作コンポーネントはこのように呼び出せます。
import UserBox from "./UserBox"

// WhiteBoardコンポーネントの定義
class WhiteBoard extends React.Component {
  // コンストラクタ
  constructor(props) {
    // おまじない
    super(props);

    // いくつか省略

    // イベントハンドラのバインド
    this.onTaskSave = this.onTaskSave.bind(this); // タスク更新時の処理
  }

  // またまた省略

  // タスク更新イベント処理
  onTaskSave(task) {
    // タスク更新処理(tasks/update)を呼び出します。
    fetch(this.props.update_task_url, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json; charset=utf-8",
        "X-CSRF-Token": this.props.secure_token
      },
      body: JSON.stringify(task)
    })
    .then(response => response.json())
    .then(json => {
      /* 実は返されたデータの使い道を見失った。。 */
      console.log(JSON.stringify(json));
    })
    .catch(error_response => console.log(error_response));
  }

  // レンダリング
  // UserBoxのpropsにonTaskSaveを追加しました。
  render () {
    return (
      <React.Fragment>
        <div id="WhiteBoardFlame">
          <div id="WhiteBoardTitle">{this.props.title}</div>
          <div id="AddUserButton" onClick={this.onAddUserClick} >+Add User</div>
          { this.state.show_add_user && <UserForm onAddButtonClick={this.ExecuteAddUser} onCancelButtonClick={this.CancelAddUser} /> }
          <div id="WhiteBoard">
            { ! this.state.loading && this.state.users.map((user) => <UserBox user={user} key={user.id} dropHandlerRegister={this.dropHandlerRegister} onTaskDrop={this.onTaskDrop} onTaskSave={this.onTaskSave} /> )}
          </div>
        </div>
      </React.Fragment>
    );
  }
}

UserBox.jsの修正

ここにも、onTaskSaveというメソッドを追加してあげます。

app/javascript/components/UserBox.js
// ユーザ毎の箱を表示します。
class UserBox extends React.Component {

  // コンストラクタです。
  constructor(props) {
    // おまじない
    super(props);

    // 色々省略
    // イベントハンドラのバインド
    this.onTaskSave = this.onTaskSave.bind(this); // <-- onTaskSaveメソッドのバインドを追加します。

  }

  // タスク更新処理
  onTaskSave(task) {
    // stateに保持しているタスクデータを更新
    var tasks = this.state.tasks;
    tasks[task.id] = task;

    // さらにコールバック(この中でDB反映が行われます。)
    this.props.onTaskSave(task);

    // stateを更新
    this.setState({tasks: tasks});

  }

  // レンダリング
  render () {
    return (
      <React.Fragment>
        <div id={"user-" + this.props.user.id} className="UserBox" onDrop={this.onDrop} onDragOver={this.preventDefault} >
          <div className="UserName">{this.props.user.name}</div>
          <div className="TaskArea">
            {
              Object.keys(this.state.tasks).map(
               (key) => <Sticky 
                          user_id={this.props.user.id} 
                          task={ this.state.tasks[key] } 
                          key={ key }
                          onTaskSave={this.onTaskSave} /> 
              ) 
            }
          </div>
        </div>
      </React.Fragment>
    );
  }

Sticky.jsの修正

やっとここまできましたー。
最初に書いたonSaveButtonClickの中身をちゃんと書いてみます。

app/javascript/components/Sticky.js
  // 保存ボタンクリックイベントハンドラ
  onSaveButtonClick(event){
    // 以下のように"current.textContent"で取得した値を使って、
    // UserBoxのonTaskSave()を呼び出して(コールバックして)あげます。
    var task = this.props.task;
    task.title = this.taskTitle.current.textContent;
    this.props.onTaskSave(task);

    // title以外の値も同じように更新できます。

  }

テストじゃぁ!!

ここでも、しつこくテストを追加していきます。
UI側は、systemテストでやるのが楽です。

test/system/whiteboards_test.rb
  test "sticky is able to update" do
    # fixutreで登録したデータを取得しておきます。
    task1 = tasks(:task1);

    # divのidを設定します。
    task1_id = "task-" + task1.id.to_s;

    # white_board/mainを開く。
    visit white_board_main_url;

    # task1の要素を取得します。
    div_task1 = find(id: task1_id);

    # title要素を取得します。
    div_title1 = div_task1.find("div", class: "TaskTitle");

    # title要素の中身を書き換えます。(text=でできるかと思ったら、setでした。)
    div_title1.set("task1_updated!!");

    # saveボタンを押しちゃいます。
    div_task1.find("div", class: "TaskUpdateButton").click();

    # 表示されている値が更新されていますように。
    assert_equal("task1_updated!!", div_title1.text);

    # DBに反映されていますように!!
    task1_updated = Task.find(task1.id);
    assert_equal("task1_updated!!", task1_updated.title); 

  end

まとめ

  1. Reactでレンダリングした要素でcontentEditable=trueを使いたい場合は、react-contenteditableを使うと良い。
  2. しかし、今回の場合はinputタグでスタイル指定してそれっぽく見せたほうが良かったのではないかと。
  3. ContentEditableで描画した部分も普通にCapybaraでテストできる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

華麗なるGatsby.jsの実践(認証機能をつけてみよう)

Gatsby.jsに認証機能などの動的な機能をつけるにはどうすればいいのだろう?
と思って、公式を参考にしつつ認証機能のサンプルコードを実装してみました。
なお、あくまでサンプルですので、パスやユーザーはgatsby.js内にハードコーディングされています。

リポジトリは以下になります。
https://github.com/takanokana/gatsby-practice

【参考公式ページ】
https://www.gatsbyjs.org/docs/react-hydration
https://www.gatsbyjs.org/docs/adding-app-and-website-functionality/
https://www.gatsbyjs.org/docs/client-only-routes-and-user-authentication/#implementing-client-only-routes

React Hydration

Gatsbyは、HTMLを静的に生成する静的サイトジェネレーターとしての機能、それに加えて、
生成したHTMLを、 React hydrationを通してクライアントサイドで拡張し、アプリのような振る舞いを持たせます。

上記の機能とGatsbyに付属している@reach/routerを使用し、client only routes,つまり静的ページとしては吐き出さないページを作ることができます。

認証機能においては、Gatsbyが生成した静的HTMLはファイルサーバ上にあるので、制御が不可能です。(ユーザーが直接URLを入力するとアクセスできてしまう)
なので、client only routesを使用することでユーザーをルーティングさせ、アクセスを制限することが必要となります。

src/pages/app.js
import React from "react"
import { Router } from "@reach/router"
import Auth from "../components/Auth"
const App = () => {
  return(
    <div>
      <Router basepath="/app">
        <Auth path="/" />
      </Router>
    </div>
  )
}

export default App
src/pages/Auth.js
import React from "react"

export default function Auth() {
  return (
    <div>認証ページ</div>
  )
}

以上のコーディングで、 localhost:8000/appにアクセスすると、認証ページ、と記述されたページを出すことができます。

またGatsbyではビルドがNode.jsで実行される関係でビルド時にlocalStoragewindowを使うことができません。しかし、外部認証サービスなどの中にはlocalStorageやwindowといったものにアクセスするものもあります。

なので、ビルド中に不具合を起こさないため、該当コードをラッピングする必要があります。

import app from "firebase/app"
if (typeof window !== 'undefined'){
  app.initializeApp(config)
 }

onCreatePage

gatsby-node.jsを編集して、/app/が制限された区画であることを定義して、必要に応じてページを作成するようにします。
onCreatePageは、全てのページが作成された後に呼ばれます。
matchPathで指定された部分は、build時に生成しないようになります。

gatsby-node.js
exports.onCreatePage = async({ page, actions }) => {
  const { createPage } = actions

  if(page.path.match(/^\/app/)){
    page.matchPath = "/app/*"
    createPage(page)
  }
}

実例

実際に、仮の認証システムをjs上で用意して、認証機能をつけてみます。
下記src/service/auth.jsで実装する機能は、本来ならばfirebaseなどが受け持ちます。

src/service/auth.js
export const isBrowser = () => typeof window !== "undefined"

export const getUser = () =>
 isBrowser() && window.localStorage.getItem("gatsbyUser")
 ? JSON.parse(window.localStorage.getItem('gatsbyUser'))
 : {}
const setUser = user =>
 window.localStorage.setItem("gatsbyUsr", JSON.stringify(user))

export const handleLogin = ({ username, password }) => {
 if (username === `join` && password === `pass`){
   return setUser({
         username: `join`,
         name: `Johnny`,
         email: `johnny@example.com`
    })
 }
 return false
}

export const isLoggedIn = () => {
  const user = getUser()

  return !!user.username
}
export const logout = callback => {
  setUser({})
  callback()
}

app.jsを下記のようにします。

app.js
import React from "react"
import { Router } from "@reach/router"
import Auth from "../components/Auth"
import PrivateRoute from "../components/PrivateRoute"
import Secret from "../components/Secret"


const App = () => {
  return(
    <div>
      <Router basepath="/app">
        <PrivateRoute path="/secret" component={Secret} />
        <Auth path="/login" />
      </Router>
    </div>
  )
}

export default App

PrivateRouteは下記のようなHOCとなっています。

src/components/PrivateRoute
import React from "react"
import { navigate } from "gatsby"
import { isLoggedIn } from "../service/auth"

const PrivateRoute = ({ component: Component, location, ...rest}) => {
  if (!isLoggedIn() && location.pathname !== `/app/login`) {
    navigate("/app/login")
    return null
  }

  return <Component {...rest} />
}

export default PrivateRoute

navigate (https://www.gatsbyjs.org/docs/gatsby-link/) ですが、
送信後、サンクスページに移動するといった用途に使用できます。stateを渡すことなども可能です。
PrivateRouteをかますことで、ログインしていなければ /app/loginへ、ログインしていれば該当ページへ飛ぶ、といった制限付きのルーティングが実現します。

ログインページは下記のように実装しました。

src/components/Auth.js
import React, { Component } from "react"
import { handleLogin, isLoggedIn} from "../service/auth"
import { navigate, Link } from "gatsby"

export default class Auth extends Component {
  state = {
    username: ``,
    password: ``
  }


## 実際のページ

  handleUpdate(event) {
    this.setState({
      [event.target.name]: event.target.value
    })
  }
  handleSubmit(event) {
    event.preventDefault()
    handleLogin(this.state)
    navigate(`/app/secret`)
  }
  render() {
    return (
      <div>
        認証ページ
        {isLoggedIn() ?
        <Link
          to="/app/secret"
        >認証後ページへ</Link>
        :
        <>
          <dl>
            <dt>名前</dt>
            <dd>
              <input
                name="username"
                onChange={e => this.handleUpdate(e)}
              ></input>
            </dd>
          </dl>
          <dl>
            <dt>パスワード</dt>
            <dd>
              <input
                name="password"
                onChange={e => this.handleUpdate(e)}
              />
            </dd>
          </dl>
          <button
          type="submit"
          onClick={e => this.handleSubmit(e)}
        >送信</button>
        </>
        }
      </div>
    )
  }
}

上記により、名前とパスワードが正しい状態でログインボタンを押すと、(ここではhandleLoginで判定されている、 john/pass)認証後ページであるSecret.jsに飛ぶことができます。

認証後ページは、下記のようにログアウト機能もいれました。

Secret.js
import React from "react"
import { logout } from "../service/auth"
import { navigate } from "gatsby"
export default function Auth() {
  const logoutHandler = () => {
    navigate('/')
    return
  }
  return (
    <div>認証後ページ ?

    <button
      type="button"
      onClick={e => logout(logoutHandler)}
    >ログアウトする</button>
    </div>
  )
}

実際の挙動

このようになります。
たとえ直接 /app/secret と打ち込んでも、ログインされていなければsecretは見ることができません。
ezgif-6-a9de48eb2daa.gif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】ReactでToDoアプリを作ってみた

概要

今回はアプリ制作の登竜門とも言える、ToDoアプリを作成してみました。
実はこれ、単純に見えて意外と複雑!
でもReactでstate・propsなどの流れを確認するにはちょうどいいレベルですね。

完成形はこちら

ezgif.com-optimize.gif

搭載機能

大きく分けるとこんな感じです。
スクリーンショット 2020-05-24 14.34.42.png

開発環境

・macOS Catalina ver.10.15.4
・Editor:VScode
・node.js(create-react-app)

全体図

スクリーンショット 2020-05-24 15.14.12.png

主に使うやつだけ載せました!
ざっとですが、流れとしてはindex.html←index.js←App.js・index.css←componentsって感じです。
ここの位置関係をちゃんと把握しておくと、後々楽になってきます。

解説

今回はUIのパーツごと・ファイルごとにコードの成り立ちを解説している形になっています。
ですので、実際に作っていく流れとは違うとは思います!ご了承を。

1.TodoHeader.jsx
2.TodoForm.jsx
3.TodoList.jsx
4.App.js
5.index.css

1.TodoHeader.jsx

これはリストの最上部を描画しているコンポーネントです。
ここで実装されている主な機能は
・複数で削除が可能、Alert機能
・Check/総数 でカウント
ですね。
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3631313733342f64303531373637312d663561652d356533652d323161342d3938313230303834643231302e706e67.png
全体のコードはこちら!

JSX.js
import React from 'react';

export default function Todoheader(props) {
    const remaining = props.todos.filter((todo) => {
        return !todo.isDone;
    });
    return (
        <h1>
            <button onClick={props.purge}>Purge</button>
        Today's Task
            <span>
                ({remaining.length}/{props.todos.length})
        </span>
        </h1>
    );
}

remainingではfilterで、isDone=false=□ の反対(!)であるチェックされた項目だけが集められています。これによって{remaining.length}/{props.todos.length}で チェック/全体 を表示しています。
isDone,purge,todosについては4.App.jsからの引用なのでそちらで紹介をします。

2.TodoForm.jsx

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3631313733342f64303531373637312d663561652d356533652d323161342d3938313230303834643231302e706e67.png
これはリストの中部を描画しているコンポーネントです。
ここで実装されている主な機能は
・チェックをつけたら斜線が入る
・何もToDoがない時はPerfectを表示
・単独で削除が可能、Alert機能
ですね。
全体のコードはこちら!

JSX.js
import React from 'react';

export default function TodoList(props) {
    const todos = props.todos.map((todo) => {
        return (
            <TodoItem
                key={todo.id}
                todo={todo}
                checkTodo={props.checkTodo}
                deleteTodo={props.deleteTodo}
            />
        );
    });
    return <ul>{props.todos.length ? todos : <li>Perfect!</li>}</ul>;
}

function TodoItem(props) {
    return (
        <li>
            <label>
                <input
                    type="checkbox"
                    checked={props.todo.isDone}
                    onChange={() => props.checkTodo(props.todo)}
                />
                <span className={props.todo.isDone ? "done" : ""}>
                    {props.todo.title}
                </span>
            </label>
            <span className="cmd" onClick={() => props.deleteTodo(props.todo)}>
                [×]
        </span>
        </li>
    );
}

ここではTodoListで要素をコピーし、TodoItemで実際に処理を行っていく流れになります。
まずmapメソッドを用いてtodosをコピーします。その際にTodoItemにkeyやtodoなどの属性を付与するのですが、checkTodo,deleteTodoについては後で見ていきます。

簡単に言うとcheckTodoはチェックボックスを使えるようにするため、deleteTodoは消せるようにするためのメソッドです。
ulでは、要素があったらtodosを表示、なかったらPerfect!を表示できるように演算子を使用します。

最初に出てくるspanではcssで斜線を引けるようにtrueの時にdoneクラスを付与します。
次に出てくるspanではクリックで要素を消せるように、onClickで機能を追加しています。

3.TodoList.jsx

これはリストの最下部を描画しているコンポーネントです。
ここで実装されている主な機能は
・入力+Add ボタンで追加が可能
ですね。
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3631313733342f64303531373637312d663561652d356533652d323161342d3938313230303834643231302e706e67.png
全体のコードはこちら!

JSX.js
import React from 'react';

export default function TodoForm(props) {
    return (
        <form onSubmit={props.addTodo}>
            <input type="text" value={props.item} onChange={props.updateItem} />
            <input type="submit" value="Add" />
        </form>
    );
}

ここではaddTodo,updateItemメソッドを用いて実際に記入した要素が反映されるようにしていますね。
これもApp.jsで見ていきましょう(丸投げ)

4.App.js

ここでは上3つのコンポーネントが集まり、stateを用いた機能を補完しています
さらにプラスで実装されている主な機能は
・リロードしても記録が残る
ですね。
全体のコードはこちら!
このコンポーネントは複雑なので注釈をつけていくスタイルにします。

JSX.js
import React from "react";
import TodoForm from "./components/TodoForm";
import TodoList from "./components/TodoList";
import Todoheader from "./components/Todoheader";

const todos = [];

function getUniqueId() {
  //乱数を発生させて、Itemに一意の番号を付与しています
  return new Date().getTime().toString(36) + Math.random().toString(36);
}

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      todos: todos,
      item: "",
    };
    this.deleteTodo = this.deleteTodo.bind(this);
    this.checkTodo = this.checkTodo.bind(this);
    this.updateItem = this.updateItem.bind(this);
    this.addTodo = this.addTodo.bind(this);
    this.purge = this.purge.bind(this);
  }

  purge() {
    if (!window.confirm("Are you sure?")) {
      //falseならreturnを返す
      return;
    }
    const todos = this.state.todos.filter((todo) => {
      //Trueのときの判定
      //filterでToDoにtodosの1つ1つが入っていって
      //まだチェックしていないToDoだけを集めて更新する
      // =>チェックされている項目だけが消えているように見える
      return !todo.isDone;
    });
    this.setState({
      todos: todos,
    });
  }

  addTodo(e) {
    e.preventDefault(); //(*1)

    if (this.state.item.trim() === "") {
      return; //空文字を処理しない(ToDoに何もない要素が追加されないようにする)
    }

    const item = {
      id: getUniqueId(),
      title: this.state.item,
      isDone: false, // t/fを判断する箱。
                     // 初期状態をfalseにすることで、チェックボックスが□で出てくる
    };

    const todos = this.state.todos.slice();
    //オブジェクトのプロパティをいじらない時のコピーなのでslice
    todos.push(item);
    this.setState({
      todos: todos,
      item: "", //更新した時に空にする
    });
  }

  checkTodo(todo) {
    const todos = this.state.todos.map((todo) => {
      return { id: todo.id, title: todo.title, isDone: todo.isDone };
    });
    //オブジェクトのコピーはmapで行う。todosはstateなので直接変更できない

    const pos = this.state.todos
      .map((todo) => {
        return todo.id;
      })
      .indexOf(todo.id);
    //idのみのtodoをmapで配列に集め、indexOfで渡されてきたtodoが何番目かを最終的な値とする

    todos[pos].isDone = !todos[pos].isDone;
    //取ってきた値のisDone(t/f判定)が反転できるようにする

    this.setState({
      todos: todos,
    });
    //それらを全てstateに反映する
  }

  deleteTodo(todo) {
    if (!window.confirm("Are you sure?")) {
      return;
    }
    const todos = this.state.todos.slice(); 
   //オブジェクトのプロパティをいじらない時のコピーなのでslice
    const pos = this.state.todos.indexOf(todo);
    todos.splice(pos, 1); //pos番目の要素を1つ取り除く
    this.setState({
      todos: todos,
    });
  }

  updateItem(e) {
    this.setState({
      item: e.target.value,
    });
//formの値はイベントオブジェクトから取得できるので、eを引数にしつつthis.setState()として、
//stateの中のitemはformのtarget.valueとするとformに入力された値がUIに反映される
  }

//(*2)ここはリロードしても値を保持するデータの永続化を行っています。
  componentDidUpdate() {
    localStorage.setItem("todos", JSON.stringify(this.state.todos));
  }//ここでlocalStorageに値を保持し
  componentDidMount() {
    this.setState({
      todos: JSON.parse(localStorage.getItem("todos")) || [],
    });//ここで値を読み込ませています
  }

  render() {
    return (
      <div className="container">
        <Todoheader
          todos={this.state.todos}
          purge={this.purge}
        />
        <TodoList
          todos={this.state.todos}
          checkTodo={this.checkTodo}
          deleteTodo={this.deleteTodo}
        />
        <TodoForm
          item={this.state.item}
          updateItem={this.updateItem}
          addTodo={this.addTodo}
        />
      </div>
    );
  }
}

export default App;

*1:(https://qiita.com/tochiji/items/4e9e64cabc0a1cd7a1ae)
*2:(https://qiita.com/jima-r20/items/73b78c4c8cf5af2fed58)

5.index.css

ここでは全体の見た目を整えています。
今回は特にフレームワーク等を使わずに書いているので、コードだけ載せて省略させていただきます。
全体のコードはこちら!

JSX.js
body {
  font-size: 16px;
  font-family: Arial, Helvetica, sans-serif;
}

.container {
  width: 300px;
  margin: auto;
}

.container h1 {
  font-size: 16px;
  border-bottom: 1px solid #ddd;
  padding: 16px 0;
}

.container ul {
  padding: 0;
  list-style: none;
}
.container li {
  line-height: 1.5;
}

.container input[type="checkbox"] {
  margin-right: 8px;
}
.container input[type="text"] {
  padding: 2px;
  margin-right: 5px;
}

h1 > span {
  color: #ccc;
  font-size: 12px;
  font-weight: normal;
  margin-left: 7px;
}

h1 > button {
  float: right;
}

.cmd {
  font-size: 12px;
  cursor: pointer;
  color: #08c;
  margin-left: 5px;
}

.done {
  text-decoration: line-through;
  color: #ccc;
}

終わり

少し後半雑になってしまったのは自分自身、完璧に理解できていないからですね
もっと噛み砕いて説明できるように精進したいなと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React の React.memo、useCallback、useMemo の使い方、使い所を理解してパフォーマンス最適化をする

はじめに

React(v16.12.0)のReact.memouseCallbackuseMemoの基本的な使い方、使い所に関しての備忘録です。

デモは CodeSandbox 上に置いてあります。編集して動作を確認してみると理解が深まると思います。

本記事で用いている用語

  • メモ化
  • 計算結果

メモ化

計算結果を保持し、それを再利用する手法のこと。

キャッシュのようなものだとイメージすれば良いと思う。

そのため、以下の言葉の意味は大体同じ。

  • 「メモ化された値」=「計算結果が保持された値」
  • 「メモ化する」=「計算結果を再利用できるように保持する」

メモ化によって都度計算する必要がなくなるため、パフォーマンスの向上が期待できる。

計算結果

以下のような計算の結果のこと。

// result は 1 + 2 の計算結果を格納している変数
const result = 1 + 2;

// result2 は [1, 2, 3, 4, 5].map(number => number * 2) の計算結果を格納している変数
const result2 = [1, 2, 3, 4, 5].map(number => number * 2);

// result3 は React.createElement("div", null, `Hello ${this.props.name}`) の計算結果を格納している変数
const result3 = React.createElement("div", null, `Hello ${this.props.name}`);

React におけるパフォーマンス最適化

React では、不要な再計算やコンポーネントの再描画を抑えることが、パフォーマンス最適化の基本的な戦略となる。

それらを実現する手段としてReact.memouseCallbackuseMemoを利用する。

React 以外のパフォーマンスチューニングにも言えることだが、計測は必須。

無闇に利用してもパフォーマンスが向上するわけではなく、意味がない場合もあるため注意。

React.memo

コンポーネント(コンポーネントの描画結果)をメモ化する React の API(メソッド)。

コンポーネントをメモ化することで、コンポーネントの再描画をスキップできる。

なぜ React.memo を利用するのか

以下のようなコンポーネントの再描画をスキップすることで、パフォーマンスの向上が期待できるから。

  • 描画コストが高いコンポーネント
  • 頻繁に再描画されるコンポーネント内の子コンポーネント

通常のコンポーネントに対しては、わざわざReact.memoを利用する必要はない。

React.memo の構文

React.memo(コンポーネント);

例えば、Helloというコンポーネントをメモ化する場合は以下のようになる。

const Hello = React.memo(props => {
  return <h1>Hello {props.name}</h1>;
});

React.memoは Props の等価性(値が等価であること)をチェックして再描画の判断をする。

新しく渡された Props と前回の Props を比較し、等価であれば再描画をせずにメモ化したコンポーネントを再利用する。

そのため、上記のHelloコンポーネントの場合、props.nameが更新されない限りコンポーネントは再描画されない。

React.memo の利用例

React.memoを利用する場合と、しない場合では何が違うのか比較してみる。

React.memo を利用しない場合

通常、コンポーネントの state が更新されると、そのコンポーネントは再描画される。

以下のデモのように親コンポーネントが再描画されると、その子コンポーネントも常に再描画される。
react-memo.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = props => {
  console.log("render Child");
  return <p>Child: {props.count}</p>;
};

export default function App() {
  console.log("render App");
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  );
}

これが通常の挙動なので、この書き方が悪いわけではなく、問題もない。

コンポーネントの不要な再描画でパフォーマンスの問題が発生した場合、React.memoの利用を検討する。

今回はChildコンポーネントが常に再描画されても何も問題はないため、React.memoを利用する必要はない。

React.memo を利用する場合

以下はReact.memoを利用し、Childコンポーネントの再描画をスキップしているデモ。
using-react-memo.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <p>Child: {props.count}</p>;
});

export default function App() {
  console.log("render App");

  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  );
}

count1を更新してAppコンポーネントを再描画した時は、Childコンポーネントに渡される Props(count2)は更新されないため、再描画はスキップされる。

Childコンポーネントに渡されるcount2が更新された時だけ、再描画されるようになった。

描画コストが高いコンポーネントをメモ化する

極端な例になるが、以下のデモのように描画コストが高いコンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
using-react-memo-02.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = React.memo(props => {
  let i = 0;
  while (i < 1000000000) i++;
  console.log("render Child");
  return <p>Child: {props.count}</p>;
});

export default function App() {
  console.log("render App");

  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  );
}

頻繁に再描画されるコンポーネント内の子コンポーネントをメモ化する

以下のデモのように、頻繁に再描画されるコンポーネント内の子コンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
using-react-memo-03.gif
デモを見る

App.js
import React, { useState, useEffect, useRef } from "react";

const Child = React.memo(() => {
  console.log("render Child");
  return <p>Child</p>;
});

export default function App() {
  console.log("render App");

  const [timeLeft, setTimeLeft] = useState(100);
  const timerRef = useRef(null);
  const timeLeftRef = useRef(timeLeft);

  useEffect(() => {
    timeLeftRef.current = timeLeft;
  }, [timeLeft]);

  const tick = () => {
    if (timeLeftRef.current === 0) {
      clearInterval(timerRef.current);
      return;
    }
    setTimeLeft(prevTime => prevTime - 1);
  };

  const start = () => {
    timerRef.current = setInterval(tick, 10);
  };

  const reset = () => {
    clearInterval(timerRef.current);
    setTimeLeft(100);
  };

  return (
    <>
      <button onClick={start}>start</button>
      <button onClick={reset}>reset</button>
      <p>App: {timeLeft}</p>
      <Child />
    </>
  );
}

コールバック関数を Props として受け取ったコンポーネントは必ず再描画される

以下のデモのようにコールバック関数を受け取ったコンポーネントはReact.memoを利用しても必ず再描画される。
using-react-memo-04.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <button onClick={props.handleClick}>Child</button>;
});

export default function App() {
  console.log("render App");

  const [count, setCount] = useState(0);
  // 関数はコンポーネントが再描画される度に再生成されるため、
  // 関数の内容が同じでも、新しい handleClick と前回の handleClick は
  // 異なるオブジェクトなので、等価ではない。
  // そのため、コンポーネントが再描画される。
  const handleClick = () => {
    console.log("click");
  };

  return (
    <>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
      <Child handleClick={handleClick} />
    </>
  );
}

以下のように参照が異なる関数は別のオブジェクトとなる。

function doSomething() {
  console.log("doSomething");
}
const func1 = doSomething;
const func2 = doSomething;
console.log(doSomething === doSomething); // true
console.log(func1 === func2); // true

const func3 = () => {
  console.log("doSomething");
};
const func4 = () => {
  console.log("doSomething");
};
console.log(func3 === func4); // false

前述のhandleClickが参照する関数も、Appコンポーネントが再描画される度に再生成されるため、等価ではない。

そのため、関数の内容が同じでもChildコンポーネントが再描画される。

この問題を解消するためには、useCallbackを利用して関数をメモ化する必要がある。

useCallback

メモ化されたコールバック関数を返すフック。

なぜ useCallback を利用するのか

React.memoと併用することで、コンポーネントの不要な再描画をスキップできるから。

より具体的に言えば、React.memoでメモ化したコンポーネントにuseCallbackでメモ化したコールバック関数を Props として渡すことで、コンポーネントの不要な再描画をスキップできるから。

useCallbackを単体で利用したり、メモ化したコールバック関数を Props として渡さないと意味はないので注意。

useCallback の構文

useCallback(コールバック関数, 依存配列);

依存配列とは、コールバック関数が依存している要素が格納された配列のこと。

例えば、countという変数をconsole.logで出力する関数をメモ化したい場合は以下のようになる。

const callback = useCallback(() => console.log(count), [count]);

依存している要素が更新されれば、関数が再生成される。

依存している要素がなければ、依存配列は空で OK。

const callback = useCallback(() => console.log("doSomething"), []);

useCallback の利用例

以下はメモ化したコールバック関数を渡し、コンポーネントは再描画をスキップしているデモ。
using-usecallback.gif
デモを見る

App.js
import React, { useState, useCallback } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <button onClick={props.handleClick}>Child</button>;
});

export default function App() {
  console.log("render App");

  const [count, setCount] = useState(0);
  // 関数をメモ化すれば、新しい handleClick と前回の handleClick は
  // 等価になる。そのため、Child コンポーネントは再描画されない。
  const handleClick = useCallback(() => {
    console.log("click");
  }, []);

  return (
    <>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
      <Child handleClick={handleClick} />
    </>
  );
}

useMemo

メモ化された値を返すフック。

コンポーネントの再描画時に値を再利用できる。

なぜ useMemo を利用するのか

値を再利用することで、値の不要な再計算をスキップできるから。

不要な再計算をスキップすることにより、パフォーマンスの向上が期待できるから。

useMemo の構文

useMemo(() => 値を計算するロジック, 依存配列);

依存配列とは、値を計算するロジックが依存している要素(値の計算に必要な要素)が格納された配列のこと。

例えば、countという変数の値を2倍にした値をメモ化したい場合は以下のようになる。

const result = useMemo(() => count * 2, [count]);

依存している要素が更新されれば、値が再計算される。

useMemo の利用例

useMemoを利用する場合と、しない場合では何が違うのか比較してみる。

useMemo を利用しない場合

以下はuseMemoを利用せず、不要な再計算が発生しているデモ
unused-usememo.gif
デモを見る

App.js
import React, { useState } from "react";

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // 引数の数値を2倍にして返す。
  // 不要なループを実行しているため計算にかなりの時間がかかる。
  const double = count => {
    let i = 0;
    while (i < 1000000000) i++;
    return count * 2;
  };

  // count2 を2倍にした値
  // double(count2) はコンポーネントが再描画される度に実行されるため、
  // count1 を更新してコンポーネントが再描画された時にも実行されてしまう。
  // そのため、count1 を更新してコンポーネントを再描画する時も時間がかかる。
  // count1 を更新しても doubledCount の値は変わらないため、count1 を更新した時に
  // double(count2) を実行する意味がない。したがって、不要な再計算が発生している状態である。
  // count1 が更新されてコンポーネントが再描画された時は double(count2) が実行されないようにしたい。
  const doubledCount = double(count2);

  return (
    <>
      <h2>Increment count1</h2>
      <p>Counter: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment count1</button>

      <h2>Increment count2</h2>
      <p>
        Counter: {count2}, {doubledCount}
      </p>
      <button onClick={() => setCount2(count2 + 1)}>Increment count2</button>
    </>
  );
}

count1を更新した時もdouble(count2)が実行されてしまうため、count1を更新してコンポーネントを再描画する時も時間がかかる。

useMemo を利用する場合

以下はuseMemoを利用し、不要な再計算をスキップするデモ。
using-usememo.gif
デモを見る

App.js
import React, { useState, useMemo } from "react";

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // 引数の数値を2倍にして返す。
  // 不要なループを実行しているため計算にかなりの時間がかかる。
  const double = count => {
    let i = 0;
    while (i < 1000000000) i++;
    return count * 2;
  };

  // count2 を2倍にした値をメモ化する。
  // 第2引数に count2 を渡しているため、count2 が更新された時だけ値が再計算される。
  // count1 が更新され、コンポーネントが再描画された時はメモ化した値を利用するため再計算されない。
  const doubledCount = useMemo(() => double(count2), [count2]);

  return (
    <>
      <h2>Increment(fast)</h2>
      <p>Counter: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment(fast)</button>

      <h2>Increment(slow)</h2>
      <p>
        Counter: {count2}, {doubledCount}
      </p>
      <button onClick={() => setCount2(count2 + 1)}>Increment(slow)</button>
    </>
  );
}

useMemoを利用して値をメモ化したため、count1を更新した時はdouble(count2)が実行されないようになった。

そのため、count1を更新した時のコンポーネントの再描画が高速になった。

依存配列は正しく指定する必要がある

useCallbackuseMemoの依存配列は正しく指定しないとバグの原因になる。

そのため、以下のコードは NG。

// 依存要素である count2 が依存配列にないため NG
const result = useMemo(() => count * count2, [count]);

// これが正しい
// const result = useMemo(() => count * count2, [count, count2]);

そのため、eslint-plugin-react-hooksなどを利用して、必ず Lint する。

使い所

パフォーマンスを計測し、ボトルネックになっている箇所に適用していくのが一番効果的。

とは言え、GitHub 上にあるコードや技術書を見てみると、ガンガン利用していた。

厳密な利用基準を設けるのも大変なので、それぞれの機能や役割をちゃんと理解しているのであれば、積極的に利用しても大きな問題はないと思った。

終わり

今回準備したデモは極端な例ですが、利用シーンによっては非常に有用な機能です。

状況に応じて利用していきましょう。

お知らせ

KDP(Kindle ダイレクト・パブリッシング)を利用して、技術書を出版しました。

どちらも 500 円(Kindle Unlimited だったら無料)です。

書籍の詳細は商品ページからご覧いただけます。

興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする

はじめに

React(v16.12.0)のReact.memouseCallbackuseMemoの基本的な使い方、使い所に関しての備忘録です。

  • 「React でのパフォーマンス最適化の手段を知りたい」
  • 「なぜReact.memouseCallbackuseMemoを利用するのかわからない」

といった人達向けに書いた記事です。

デモは CodeSandbox 上に置いてあります。編集して動作を確認してみると理解が深まると思います。

本記事で用いている用語

  • メモ化
  • 計算結果

メモ化

計算結果を保持し、それを再利用する手法のこと。

キャッシュのようなものだとイメージすれば良いと思う。

そのため、以下の言葉の意味は大体同じ。

  • 「メモ化された値」=「計算結果が保持された値」
  • 「メモ化する」=「計算結果を再利用できるように保持する」

メモ化によって都度計算する必要がなくなるため、パフォーマンスの向上が期待できる。

計算結果

以下のような計算の結果のこと。

// result は 1 + 2 の計算結果を格納している変数
const result = 1 + 2;

// result2 は [1, 2, 3, 4, 5].map(number => number * 2) の計算結果を格納している変数
const result2 = [1, 2, 3, 4, 5].map(number => number * 2);

// result3 は React.createElement("div", null, `Hello ${this.props.name}`) の計算結果を格納している変数
const result3 = React.createElement("div", null, `Hello ${this.props.name}`);

React におけるパフォーマンス最適化

React では、不要な再計算やコンポーネントの再レンダリングを抑えることが、パフォーマンス最適化の基本的な戦略となる。

それらを実現する手段としてReact.memouseCallbackuseMemoを利用する。

React 以外のパフォーマンスチューニングにも言えることだが、計測は必須。

無闇に利用してもパフォーマンスが向上するわけではなく、意味がない場合もあるため注意。

React.memo

コンポーネント(コンポーネントのレンダリング結果)をメモ化する React の API(メソッド)。

コンポーネントをメモ化することで、コンポーネントの再レンダリングをスキップできる。

なぜ React.memo を利用するのか

以下のようなコンポーネントの再レンダリングをスキップすることで、パフォーマンスの向上が期待できるから。

  • レンダリングコストが高いコンポーネント
  • 頻繁に再レンダリングされるコンポーネント内の子コンポーネント

通常のコンポーネントに対しては、わざわざReact.memoを利用する必要はない。

React.memo の構文

React.memo(コンポーネント);

例えば、Helloというコンポーネントをメモ化する場合は以下のようになる。

const Hello = React.memo(props => {
  return <h1>Hello {props.name}</h1>;
});

React.memoは Props の等価性(値が等価であること)をチェックして再レンダリングの判断をする。

新しく渡された Props と前回の Props を比較し、等価であれば再レンダリングをせずにメモ化したコンポーネントを再利用する。

そのため、上記のHelloコンポーネントの場合、props.nameが更新されない限りコンポーネントは再レンダリングされない。

React.memo の利用例

React.memoを利用する場合と、しない場合では何が違うのか比較してみる。

React.memo を利用しない場合

通常、コンポーネントの state が更新されると、そのコンポーネントは再レンダリングされる。

以下のデモのように親コンポーネントが再レンダリングされると、その子コンポーネントも常に再レンダリングされる。
react-memo.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = props => {
  console.log("render Child");
  return <p>Child: {props.count}</p>;
};

export default function App() {
  console.log("render App");
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  );
}

これが通常の挙動なので、この書き方が悪いわけではなく、問題もない。

コンポーネントの不要な再レンダリングでパフォーマンスの問題が発生した場合、React.memoの利用を検討する。

今回はChildコンポーネントが常に再レンダリングされても何も問題はないため、React.memoを利用する必要はない。

React.memo を利用する場合

以下はReact.memoを利用し、Childコンポーネントの再レンダリングをスキップしているデモ。
using-react-memo.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <p>Child: {props.count}</p>;
});

export default function App() {
  console.log("render App");

  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  );
}

count1を更新してAppコンポーネントを再レンダリングした時は、Childコンポーネントに渡される Props(count2)は更新されないため、再レンダリングはスキップされる。

Childコンポーネントに渡されるcount2が更新された時だけ、再レンダリングされるようになった。

レンダリングコストが高いコンポーネントをメモ化する

極端な例になるが、以下のデモのようにレンダリングコストが高いコンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
using-react-memo-02.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = React.memo(props => {
  let i = 0;
  while (i < 1000000000) i++;
  console.log("render Child");
  return <p>Child: {props.count}</p>;
});

export default function App() {
  console.log("render App");

  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  );
}

頻繁に再レンダリングされるコンポーネント内の子コンポーネントをメモ化する

以下のデモのように、頻繁に再レンダリングされるコンポーネント内の子コンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
using-react-memo-03.gif
デモを見る

App.js
import React, { useState, useEffect, useRef } from "react";

const Child = React.memo(() => {
  console.log("render Child");
  return <p>Child</p>;
});

export default function App() {
  console.log("render App");

  const [timeLeft, setTimeLeft] = useState(100);
  const timerRef = useRef(null);
  const timeLeftRef = useRef(timeLeft);

  useEffect(() => {
    timeLeftRef.current = timeLeft;
  }, [timeLeft]);

  const tick = () => {
    if (timeLeftRef.current === 0) {
      clearInterval(timerRef.current);
      return;
    }
    setTimeLeft(prevTime => prevTime - 1);
  };

  const start = () => {
    timerRef.current = setInterval(tick, 10);
  };

  const reset = () => {
    clearInterval(timerRef.current);
    setTimeLeft(100);
  };

  return (
    <>
      <button onClick={start}>start</button>
      <button onClick={reset}>reset</button>
      <p>App: {timeLeft}</p>
      <Child />
    </>
  );
}

コールバック関数を Props として受け取ったコンポーネントは必ず再レンダリングされる

以下のデモのようにコールバック関数を受け取ったコンポーネントはReact.memoを利用しても必ず再レンダリングされる。
using-react-memo-04.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <button onClick={props.handleClick}>Child</button>;
});

export default function App() {
  console.log("render App");

  const [count, setCount] = useState(0);
  // 関数はコンポーネントが再レンダリングされる度に再生成されるため、
  // 関数の内容が同じでも、新しい handleClick と前回の handleClick は
  // 異なるオブジェクトなので、等価ではない。
  // そのため、コンポーネントが再レンダリングされる。
  const handleClick = () => {
    console.log("click");
  };

  return (
    <>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
      <Child handleClick={handleClick} />
    </>
  );
}

以下のように参照が異なる関数は別のオブジェクトとなる。

function doSomething() {
  console.log("doSomething");
}
const func1 = doSomething;
const func2 = doSomething;
console.log(doSomething === doSomething); // true
console.log(func1 === func2); // true

const func3 = () => {
  console.log("doSomething");
};
const func4 = () => {
  console.log("doSomething");
};
console.log(func3 === func4); // false

前述のhandleClickが参照する関数も、Appコンポーネントが再レンダリングされる度に再生成されるため、等価ではない。

そのため、関数の内容が同じでもChildコンポーネントが再レンダリングされる。

この問題を解消するためには、useCallbackを利用して関数をメモ化する必要がある。

useCallback

メモ化されたコールバック関数を返すフック。

なぜ useCallback を利用するのか

React.memoと併用することで、コンポーネントの不要な再レンダリングをスキップできるから。

より具体的に言えば、React.memoでメモ化したコンポーネントにuseCallbackでメモ化したコールバック関数を Props として渡すことで、コンポーネントの不要な再レンダリングをスキップできるから。

useCallback の構文

useCallback(コールバック関数, 依存配列);

依存配列とは、コールバック関数が依存している要素が格納された配列のこと。

例えば、countという変数をconsole.logで出力する関数をメモ化したい場合は以下のようになる。

const callback = useCallback(() => console.log(count), [count]);

依存している要素が更新されれば、関数が再生成される。

依存している要素がなければ、依存配列は空で OK。

const callback = useCallback(() => console.log("doSomething"), []);

useCallback の利用例

以下はメモ化したコールバック関数を渡し、コンポーネントは再レンダリングをスキップしているデモ。
using-usecallback.gif
デモを見る

App.js
import React, { useState, useCallback } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <button onClick={props.handleClick}>Child</button>;
});

export default function App() {
  console.log("render App");

  const [count, setCount] = useState(0);
  // 関数をメモ化すれば、新しい handleClick と前回の handleClick は
  // 等価になる。そのため、Child コンポーネントは再レンダリングされない。
  const handleClick = useCallback(() => {
    console.log("click");
  }, []);

  return (
    <>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
      <Child handleClick={handleClick} />
    </>
  );
}

useCallback の注意点

前述の通り、useCallbackReact.memoと併用するものなので、以下のような使い方をしても意味がない(コンポーネントの不要な再レンダリングをスキップできない)ので注意。

  • React.memoでメモ化をしていないコンポーネントにuseCallbackでメモ化をしたコールバック関数を渡す
  • useCallbackでメモ化したコールバック関数を、それを生成したコンポーネント自身で利用する

React.memo でメモ化をしていないコンポーネントに useCallback でメモ化をしたコールバック関数を渡す

以下のように、メモ化をしていないコンポーネントにメモ化をしたコールバック関数を渡しても、コンポーネントは常に再レンダリングされてしまう。

import React, { useState, useCallback } from "react";

// React.memo でメモ化をしていないコンポーネントのため、メモ化されたコールバック関数を渡されても意味がない。
// App コンポーネントがレンダリングされる度に再レンダリングされる。
const Child = props => {
  console.log("render Child");
  return <button onClick={props.handleClick}>Child</button>;
};

export default function App() {
  console.log("render App");

  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => {
    console.log("click");
  }, []);

  return (
    <>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
      <Child handleClick={handleClick} />
    </>
  );
}

useCallback でメモ化したコールバック関数を、それを生成したコンポーネント自身で利用する

以下の例では、メモ化したコールバック関数をAppコンポーネント自身で利用している。

動作はするが、「コンポーネントの再レンダリングをスキップする」という目的を達成できてない。

import React, { useState, useCallback } from "react";

export default function App() {
  console.log("render App");

  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => {
    console.log("memonized callback");
  }, []);

  return (
    <>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
      <button onClick={handleClick}>logging</button>
    </>
  );
}

useMemo

メモ化された値を返すフック。

コンポーネントの再レンダリング時に値を再利用できる。

なぜ useMemo を利用するのか

値の不要な再計算をスキップすることで、パフォーマンスの向上が期待できるから。

useMemo の構文

useMemo(() => 値を計算するロジック, 依存配列);

依存配列とは、値を計算するロジックが依存している要素(値の計算に必要な要素)が格納された配列のこと。

例えば、countという変数の値を2倍にした値をメモ化したい場合は以下のようになる。

const result = useMemo(() => count * 2, [count]);

依存している要素が更新されれば、値が再計算される。

useMemo の利用例

useMemoを利用する場合と、しない場合では何が違うのか比較してみる。

useMemo を利用しない場合

以下はuseMemoを利用せず、不要な再計算が発生しているデモ
unused-usememo.gif
デモを見る

App.js
import React, { useState } from "react";

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // 引数の数値を2倍にして返す。
  // 不要なループを実行しているため計算にかなりの時間がかかる。
  const double = count => {
    let i = 0;
    while (i < 1000000000) i++;
    return count * 2;
  };

  // count2 を2倍にした値
  // double(count2) はコンポーネントが再レンダリングされる度に実行されるため、
  // count1 を更新してコンポーネントが再レンダリングされた時にも実行されてしまう。
  // そのため、count1 を更新してコンポーネントを再レンダリングする時も時間がかかる。
  // count1 を更新しても doubledCount の値は変わらないため、count1 を更新した時に
  // double(count2) を実行する意味がない。したがって、不要な再計算が発生している状態である。
  // count1 が更新されてコンポーネントが再レンダリングされた時は double(count2) が実行されないようにしたい。
  const doubledCount = double(count2);

  return (
    <>
      <h2>Increment count1</h2>
      <p>Counter: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment count1</button>

      <h2>Increment count2</h2>
      <p>
        Counter: {count2}, {doubledCount}
      </p>
      <button onClick={() => setCount2(count2 + 1)}>Increment count2</button>
    </>
  );
}

count1を更新した時もdouble(count2)が実行されてしまうため、count1を更新してコンポーネントを再レンダリングする時も時間がかかる。

useMemo を利用する場合

以下はuseMemoを利用し、不要な再計算をスキップするデモ。
using-usememo.gif
デモを見る

App.js
import React, { useState, useMemo } from "react";

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // 引数の数値を2倍にして返す。
  // 不要なループを実行しているため計算にかなりの時間がかかる。
  const double = count => {
    let i = 0;
    while (i < 1000000000) i++;
    return count * 2;
  };

  // count2 を2倍にした値をメモ化する。
  // 第2引数に count2 を渡しているため、count2 が更新された時だけ値が再計算される。
  // count1 が更新され、コンポーネントが再レンダリングされた時はメモ化した値を利用するため再計算されない。
  const doubledCount = useMemo(() => double(count2), [count2]);

  return (
    <>
      <h2>Increment(fast)</h2>
      <p>Counter: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment(fast)</button>

      <h2>Increment(slow)</h2>
      <p>
        Counter: {count2}, {doubledCount}
      </p>
      <button onClick={() => setCount2(count2 + 1)}>Increment(slow)</button>
    </>
  );
}

useMemoを利用して値をメモ化したため、count1を更新した時はdouble(count2)が実行されないようになった。

そのため、count1を更新した時のコンポーネントの再レンダリングが高速になった。

コンポーネントの再レンダリングをスキップする

useMemoはレンダリング結果もメモ化できるため、React.memoのようにコンポーネントの再レンダリングをスキップできる。

以下はコンポーネントをメモ化して、不要な再レンダリングをスキップしているデモ。
usememo-02.gif
デモを見る

App.js
import React, { useState, useMemo } from "react";

export default function App() {
  console.log("render App");
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // 引数の数値を2倍にして返す。
  // 無駄なループを実行しているため計算にかなりの時間がかかる。
  const double = count => {
    let i = 0;
    while (i < 1000000000) i++;
    return count * 2;
  };

  // レンダリング結果(計算結果)をメモ化する
  // 第2引数に count2 を渡しているため、count2 が更新された時だけ再レンダリングされる。
  // count1 が更新され、コンポーネントが再レンダリングされた時はメモ化したレンダリング結果を
  // 利用するため再レンダリングされない。
  const Counter = useMemo(() => {
    console.log("render Counter");
    const doubledCount = double(count2);

    return (
      <p>
        Counter: {count2}, {doubledCount}
      </p>
    );
  }, [count2]);

  return (
    <>
      <h2>Increment count1</h2>
      <p>Counter: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment count1</button>

      <h2>Increment count2</h2>
      {Counter}
      <button onClick={() => setCount2(count2 + 1)}>Increment count2</button>
    </>
  );
}

関数コンポーネント内でコンポーネントをメモ化したい場合はuseMemoを利用する。

以下のデモのように関数コンポーネント内でReact.memoを利用しても意味がないので注意。
meaningless-reactmemo.gif
デモを見る

App.js
import React, { useState } from "react";

export default function App() {
  console.log("render App");
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // 引数の数値を2倍にして返す。
  // 無駄なループを実行しているため計算にかなりの時間がかかる。
  const double = count => {
    let i = 0;
    while (i < 1000000000) i++;
    return count * 2;
  };

  // App コンポーネントが再レンダリングされたら
  // このコンポーネントも必ず再レンダリングされる
  const Counter = React.memo(props => {
    console.log("render Counter");
    const doubledCount = double(props.count2);

    return (
      <p>
        Counter: {props.count2}, {doubledCount}
      </p>
    );
  });

  return (
    <>
      <h2>Increment count1</h2>
      <p>Counter: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment count1</button>

      <h2>Increment count2</h2>
      <Counter count2={count2} />
      <button onClick={() => setCount2(count2 + 1)}>Increment count2</button>
    </>
  );
}

依存配列は正しく指定する必要がある

useCallbackuseMemoの依存配列は正しく指定しないとバグの原因になる。

そのため、以下のコードは NG。

// 依存要素である count2 が依存配列にないため NG
const result = useMemo(() => count * count2, [count]);

// これが正しい
// const result = useMemo(() => count * count2, [count, count2]);

そのため、eslint-plugin-react-hooksなどを利用して、必ず Lint する。

使い所

パフォーマンスを計測し、ボトルネックになっている箇所に適用していくのが一番効果的。

とは言え、GitHub 上にあるコードや技術書を見てみると、ガンガン利用していた。

厳密な利用基準を設けるのも大変なので、それぞれの機能や役割をちゃんと理解しているのであれば、積極的に利用しても大きな問題はないと思った。

終わり

今回準備したデモは極端な例ですが、利用シーンによっては非常に有用な機能です。

状況に応じて利用していきましょう。

お知らせ

KDP(Kindle ダイレクト・パブリッシング)を利用して、技術書を出版しました。

どちらも 500 円(Kindle Unlimited だったら無料)です。

書籍の詳細は商品ページからご覧いただけます。

興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React の React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする

はじめに

React(v16.12.0)のReact.memouseCallbackuseMemoの基本的な使い方、使い所に関しての備忘録です。

デモは CodeSandbox 上に置いてあります。編集して動作を確認してみると理解が深まると思います。

本記事で用いている用語

  • メモ化
  • 計算結果

メモ化

計算結果を保持し、それを再利用する手法のこと。

キャッシュのようなものだとイメージすれば良いと思う。

そのため、以下の言葉の意味は大体同じ。

  • 「メモ化された値」=「計算結果が保持された値」
  • 「メモ化する」=「計算結果を再利用できるように保持する」

メモ化によって都度計算する必要がなくなるため、パフォーマンスの向上が期待できる。

計算結果

以下のような計算の結果のこと。

// result は 1 + 2 の計算結果を格納している変数
const result = 1 + 2;

// result2 は [1, 2, 3, 4, 5].map(number => number * 2) の計算結果を格納している変数
const result2 = [1, 2, 3, 4, 5].map(number => number * 2);

// result3 は React.createElement("div", null, `Hello ${this.props.name}`) の計算結果を格納している変数
const result3 = React.createElement("div", null, `Hello ${this.props.name}`);

React におけるパフォーマンス最適化

React では、不要な再計算やコンポーネントの再描画を抑えることが、パフォーマンス最適化の基本的な戦略となる。

それらを実現する手段としてReact.memouseCallbackuseMemoを利用する。

React 以外のパフォーマンスチューニングにも言えることだが、計測は必須。

無闇に利用してもパフォーマンスが向上するわけではなく、意味がない場合もあるため注意。

React.memo

コンポーネント(コンポーネントの描画結果)をメモ化する React の API(メソッド)。

コンポーネントをメモ化することで、コンポーネントの再描画をスキップできる。

なぜ React.memo を利用するのか

以下のようなコンポーネントの再描画をスキップすることで、パフォーマンスの向上が期待できるから。

  • 描画コストが高いコンポーネント
  • 頻繁に再描画されるコンポーネント内の子コンポーネント

通常のコンポーネントに対しては、わざわざReact.memoを利用する必要はない。

React.memo の構文

React.memo(コンポーネント);

例えば、Helloというコンポーネントをメモ化する場合は以下のようになる。

const Hello = React.memo(props => {
  return <h1>Hello {props.name}</h1>;
});

React.memoは Props の等価性(値が等価であること)をチェックして再描画の判断をする。

新しく渡された Props と前回の Props を比較し、等価であれば再描画をせずにメモ化したコンポーネントを再利用する。

そのため、上記のHelloコンポーネントの場合、props.nameが更新されない限りコンポーネントは再描画されない。

React.memo の利用例

React.memoを利用する場合と、しない場合では何が違うのか比較してみる。

React.memo を利用しない場合

通常、コンポーネントの state が更新されると、そのコンポーネントは再描画される。

以下のデモのように親コンポーネントが再描画されると、その子コンポーネントも常に再描画される。
react-memo.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = props => {
  console.log("render Child");
  return <p>Child: {props.count}</p>;
};

export default function App() {
  console.log("render App");
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  );
}

これが通常の挙動なので、この書き方が悪いわけではなく、問題もない。

コンポーネントの不要な再描画でパフォーマンスの問題が発生した場合、React.memoの利用を検討する。

今回はChildコンポーネントが常に再描画されても何も問題はないため、React.memoを利用する必要はない。

React.memo を利用する場合

以下はReact.memoを利用し、Childコンポーネントの再描画をスキップしているデモ。
using-react-memo.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <p>Child: {props.count}</p>;
});

export default function App() {
  console.log("render App");

  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  );
}

count1を更新してAppコンポーネントを再描画した時は、Childコンポーネントに渡される Props(count2)は更新されないため、再描画はスキップされる。

Childコンポーネントに渡されるcount2が更新された時だけ、再描画されるようになった。

描画コストが高いコンポーネントをメモ化する

極端な例になるが、以下のデモのように描画コストが高いコンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
using-react-memo-02.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = React.memo(props => {
  let i = 0;
  while (i < 1000000000) i++;
  console.log("render Child");
  return <p>Child: {props.count}</p>;
});

export default function App() {
  console.log("render App");

  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  );
}

頻繁に再描画されるコンポーネント内の子コンポーネントをメモ化する

以下のデモのように、頻繁に再描画されるコンポーネント内の子コンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
using-react-memo-03.gif
デモを見る

App.js
import React, { useState, useEffect, useRef } from "react";

const Child = React.memo(() => {
  console.log("render Child");
  return <p>Child</p>;
});

export default function App() {
  console.log("render App");

  const [timeLeft, setTimeLeft] = useState(100);
  const timerRef = useRef(null);
  const timeLeftRef = useRef(timeLeft);

  useEffect(() => {
    timeLeftRef.current = timeLeft;
  }, [timeLeft]);

  const tick = () => {
    if (timeLeftRef.current === 0) {
      clearInterval(timerRef.current);
      return;
    }
    setTimeLeft(prevTime => prevTime - 1);
  };

  const start = () => {
    timerRef.current = setInterval(tick, 10);
  };

  const reset = () => {
    clearInterval(timerRef.current);
    setTimeLeft(100);
  };

  return (
    <>
      <button onClick={start}>start</button>
      <button onClick={reset}>reset</button>
      <p>App: {timeLeft}</p>
      <Child />
    </>
  );
}

コールバック関数を Props として受け取ったコンポーネントは必ず再描画される

以下のデモのようにコールバック関数を受け取ったコンポーネントはReact.memoを利用しても必ず再描画される。
using-react-memo-04.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <button onClick={props.handleClick}>Child</button>;
});

export default function App() {
  console.log("render App");

  const [count, setCount] = useState(0);
  // 関数はコンポーネントが再描画される度に再生成されるため、
  // 関数の内容が同じでも、新しい handleClick と前回の handleClick は
  // 異なるオブジェクトなので、等価ではない。
  // そのため、コンポーネントが再描画される。
  const handleClick = () => {
    console.log("click");
  };

  return (
    <>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
      <Child handleClick={handleClick} />
    </>
  );
}

以下のように参照が異なる関数は別のオブジェクトとなる。

function doSomething() {
  console.log("doSomething");
}
const func1 = doSomething;
const func2 = doSomething;
console.log(doSomething === doSomething); // true
console.log(func1 === func2); // true

const func3 = () => {
  console.log("doSomething");
};
const func4 = () => {
  console.log("doSomething");
};
console.log(func3 === func4); // false

前述のhandleClickが参照する関数も、Appコンポーネントが再描画される度に再生成されるため、等価ではない。

そのため、関数の内容が同じでもChildコンポーネントが再描画される。

この問題を解消するためには、useCallbackを利用して関数をメモ化する必要がある。

useCallback

メモ化されたコールバック関数を返すフック。

なぜ useCallback を利用するのか

React.memoと併用することで、コンポーネントの不要な再描画をスキップできるから。

より具体的に言えば、React.memoでメモ化したコンポーネントにuseCallbackでメモ化したコールバック関数を Props として渡すことで、コンポーネントの不要な再描画をスキップできるから。

useCallbackを単体で利用したり、メモ化したコールバック関数を Props として渡さないと意味はないので注意。

useCallback の構文

useCallback(コールバック関数, 依存配列);

依存配列とは、コールバック関数が依存している要素が格納された配列のこと。

例えば、countという変数をconsole.logで出力する関数をメモ化したい場合は以下のようになる。

const callback = useCallback(() => console.log(count), [count]);

依存している要素が更新されれば、関数が再生成される。

依存している要素がなければ、依存配列は空で OK。

const callback = useCallback(() => console.log("doSomething"), []);

useCallback の利用例

以下はメモ化したコールバック関数を渡し、コンポーネントは再描画をスキップしているデモ。
using-usecallback.gif
デモを見る

App.js
import React, { useState, useCallback } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <button onClick={props.handleClick}>Child</button>;
});

export default function App() {
  console.log("render App");

  const [count, setCount] = useState(0);
  // 関数をメモ化すれば、新しい handleClick と前回の handleClick は
  // 等価になる。そのため、Child コンポーネントは再描画されない。
  const handleClick = useCallback(() => {
    console.log("click");
  }, []);

  return (
    <>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
      <Child handleClick={handleClick} />
    </>
  );
}

useMemo

メモ化された値を返すフック。

コンポーネントの再描画時に値を再利用できる。

なぜ useMemo を利用するのか

値を再利用することで、値の不要な再計算をスキップできるから。

不要な再計算をスキップすることにより、パフォーマンスの向上が期待できるから。

useMemo の構文

useMemo(() => 値を計算するロジック, 依存配列);

依存配列とは、値を計算するロジックが依存している要素(値の計算に必要な要素)が格納された配列のこと。

例えば、countという変数の値を2倍にした値をメモ化したい場合は以下のようになる。

const result = useMemo(() => count * 2, [count]);

依存している要素が更新されれば、値が再計算される。

useMemo の利用例

useMemoを利用する場合と、しない場合では何が違うのか比較してみる。

useMemo を利用しない場合

以下はuseMemoを利用せず、不要な再計算が発生しているデモ
unused-usememo.gif
デモを見る

App.js
import React, { useState } from "react";

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // 引数の数値を2倍にして返す。
  // 不要なループを実行しているため計算にかなりの時間がかかる。
  const double = count => {
    let i = 0;
    while (i < 1000000000) i++;
    return count * 2;
  };

  // count2 を2倍にした値
  // double(count2) はコンポーネントが再描画される度に実行されるため、
  // count1 を更新してコンポーネントが再描画された時にも実行されてしまう。
  // そのため、count1 を更新してコンポーネントを再描画する時も時間がかかる。
  // count1 を更新しても doubledCount の値は変わらないため、count1 を更新した時に
  // double(count2) を実行する意味がない。したがって、不要な再計算が発生している状態である。
  // count1 が更新されてコンポーネントが再描画された時は double(count2) が実行されないようにしたい。
  const doubledCount = double(count2);

  return (
    <>
      <h2>Increment count1</h2>
      <p>Counter: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment count1</button>

      <h2>Increment count2</h2>
      <p>
        Counter: {count2}, {doubledCount}
      </p>
      <button onClick={() => setCount2(count2 + 1)}>Increment count2</button>
    </>
  );
}

count1を更新した時もdouble(count2)が実行されてしまうため、count1を更新してコンポーネントを再描画する時も時間がかかる。

useMemo を利用する場合

以下はuseMemoを利用し、不要な再計算をスキップするデモ。
using-usememo.gif
デモを見る

App.js
import React, { useState, useMemo } from "react";

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // 引数の数値を2倍にして返す。
  // 不要なループを実行しているため計算にかなりの時間がかかる。
  const double = count => {
    let i = 0;
    while (i < 1000000000) i++;
    return count * 2;
  };

  // count2 を2倍にした値をメモ化する。
  // 第2引数に count2 を渡しているため、count2 が更新された時だけ値が再計算される。
  // count1 が更新され、コンポーネントが再描画された時はメモ化した値を利用するため再計算されない。
  const doubledCount = useMemo(() => double(count2), [count2]);

  return (
    <>
      <h2>Increment(fast)</h2>
      <p>Counter: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment(fast)</button>

      <h2>Increment(slow)</h2>
      <p>
        Counter: {count2}, {doubledCount}
      </p>
      <button onClick={() => setCount2(count2 + 1)}>Increment(slow)</button>
    </>
  );
}

useMemoを利用して値をメモ化したため、count1を更新した時はdouble(count2)が実行されないようになった。

そのため、count1を更新した時のコンポーネントの再描画が高速になった。

依存配列は正しく指定する必要がある

useCallbackuseMemoの依存配列は正しく指定しないとバグの原因になる。

そのため、以下のコードは NG。

// 依存要素である count2 が依存配列にないため NG
const result = useMemo(() => count * count2, [count]);

// これが正しい
// const result = useMemo(() => count * count2, [count, count2]);

そのため、eslint-plugin-react-hooksなどを利用して、必ず Lint する。

使い所

パフォーマンスを計測し、ボトルネックになっている箇所に適用していくのが一番効果的。

とは言え、GitHub 上にあるコードや技術書を見てみると、ガンガン利用していた。

厳密な利用基準を設けるのも大変なので、それぞれの機能や役割をちゃんと理解しているのであれば、積極的に利用しても大きな問題はないと思った。

終わり

今回準備したデモは極端な例ですが、利用シーンによっては非常に有用な機能です。

状況に応じて利用していきましょう。

お知らせ

KDP(Kindle ダイレクト・パブリッシング)を利用して、技術書を出版しました。

どちらも 500 円(Kindle Unlimited だったら無料)です。

書籍の詳細は商品ページからご覧いただけます。

興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする【React】

はじめに

React(v16.12.0)のReact.memouseCallbackuseMemoの基本的な使い方、使い所に関しての備忘録です。

デモは CodeSandbox 上に置いてあります。編集して動作を確認してみると理解が深まると思います。

本記事で用いている用語

  • メモ化
  • 計算結果

メモ化

計算結果を保持し、それを再利用する手法のこと。

キャッシュのようなものだとイメージすれば良いと思う。

そのため、以下の言葉の意味は大体同じ。

  • 「メモ化された値」=「計算結果が保持された値」
  • 「メモ化する」=「計算結果を再利用できるように保持する」

メモ化によって都度計算する必要がなくなるため、パフォーマンスの向上が期待できる。

計算結果

以下のような計算の結果のこと。

// result は 1 + 2 の計算結果を格納している変数
const result = 1 + 2;

// result2 は [1, 2, 3, 4, 5].map(number => number * 2) の計算結果を格納している変数
const result2 = [1, 2, 3, 4, 5].map(number => number * 2);

// result3 は React.createElement("div", null, `Hello ${this.props.name}`) の計算結果を格納している変数
const result3 = React.createElement("div", null, `Hello ${this.props.name}`);

React におけるパフォーマンス最適化

React では、不要な再計算やコンポーネントの再描画を抑えることが、パフォーマンス最適化の基本的な戦略となる。

それらを実現する手段としてReact.memouseCallbackuseMemoを利用する。

React 以外のパフォーマンスチューニングにも言えることだが、計測は必須。

無闇に利用してもパフォーマンスが向上するわけではなく、意味がない場合もあるため注意。

React.memo

コンポーネント(コンポーネントの描画結果)をメモ化する React の API(メソッド)。

コンポーネントをメモ化することで、コンポーネントの再描画をスキップできる。

なぜ React.memo を利用するのか

以下のようなコンポーネントの再描画をスキップすることで、パフォーマンスの向上が期待できるから。

  • 描画コストが高いコンポーネント
  • 頻繁に再描画されるコンポーネント内の子コンポーネント

通常のコンポーネントに対しては、わざわざReact.memoを利用する必要はない。

React.memo の構文

React.memo(コンポーネント);

例えば、Helloというコンポーネントをメモ化する場合は以下のようになる。

const Hello = React.memo(props => {
  return <h1>Hello {props.name}</h1>;
});

React.memoは Props の等価性(値が等価であること)をチェックして再描画の判断をする。

新しく渡された Props と前回の Props を比較し、等価であれば再描画をせずにメモ化したコンポーネントを再利用する。

そのため、上記のHelloコンポーネントの場合、props.nameが更新されない限りコンポーネントは再描画されない。

React.memo の利用例

React.memoを利用する場合と、しない場合では何が違うのか比較してみる。

React.memo を利用しない場合

通常、コンポーネントの state が更新されると、そのコンポーネントは再描画される。

以下のデモのように親コンポーネントが再描画されると、その子コンポーネントも常に再描画される。
react-memo.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = props => {
  console.log("render Child");
  return <p>Child: {props.count}</p>;
};

export default function App() {
  console.log("render App");
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  );
}

これが通常の挙動なので、この書き方が悪いわけではなく、問題もない。

コンポーネントの不要な再描画でパフォーマンスの問題が発生した場合、React.memoの利用を検討する。

今回はChildコンポーネントが常に再描画されても何も問題はないため、React.memoを利用する必要はない。

React.memo を利用する場合

以下はReact.memoを利用し、Childコンポーネントの再描画をスキップしているデモ。
using-react-memo.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <p>Child: {props.count}</p>;
});

export default function App() {
  console.log("render App");

  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  );
}

count1を更新してAppコンポーネントを再描画した時は、Childコンポーネントに渡される Props(count2)は更新されないため、再描画はスキップされる。

Childコンポーネントに渡されるcount2が更新された時だけ、再描画されるようになった。

描画コストが高いコンポーネントをメモ化する

極端な例になるが、以下のデモのように描画コストが高いコンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
using-react-memo-02.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = React.memo(props => {
  let i = 0;
  while (i < 1000000000) i++;
  console.log("render Child");
  return <p>Child: {props.count}</p>;
});

export default function App() {
  console.log("render App");

  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  );
}

頻繁に再描画されるコンポーネント内の子コンポーネントをメモ化する

以下のデモのように、頻繁に再描画されるコンポーネント内の子コンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
using-react-memo-03.gif
デモを見る

App.js
import React, { useState, useEffect, useRef } from "react";

const Child = React.memo(() => {
  console.log("render Child");
  return <p>Child</p>;
});

export default function App() {
  console.log("render App");

  const [timeLeft, setTimeLeft] = useState(100);
  const timerRef = useRef(null);
  const timeLeftRef = useRef(timeLeft);

  useEffect(() => {
    timeLeftRef.current = timeLeft;
  }, [timeLeft]);

  const tick = () => {
    if (timeLeftRef.current === 0) {
      clearInterval(timerRef.current);
      return;
    }
    setTimeLeft(prevTime => prevTime - 1);
  };

  const start = () => {
    timerRef.current = setInterval(tick, 10);
  };

  const reset = () => {
    clearInterval(timerRef.current);
    setTimeLeft(100);
  };

  return (
    <>
      <button onClick={start}>start</button>
      <button onClick={reset}>reset</button>
      <p>App: {timeLeft}</p>
      <Child />
    </>
  );
}

コールバック関数を Props として受け取ったコンポーネントは必ず再描画される

以下のデモのようにコールバック関数を受け取ったコンポーネントはReact.memoを利用しても必ず再描画される。
using-react-memo-04.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <button onClick={props.handleClick}>Child</button>;
});

export default function App() {
  console.log("render App");

  const [count, setCount] = useState(0);
  // 関数はコンポーネントが再描画される度に再生成されるため、
  // 関数の内容が同じでも、新しい handleClick と前回の handleClick は
  // 異なるオブジェクトなので、等価ではない。
  // そのため、コンポーネントが再描画される。
  const handleClick = () => {
    console.log("click");
  };

  return (
    <>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
      <Child handleClick={handleClick} />
    </>
  );
}

以下のように参照が異なる関数は別のオブジェクトとなる。

function doSomething() {
  console.log("doSomething");
}
const func1 = doSomething;
const func2 = doSomething;
console.log(doSomething === doSomething); // true
console.log(func1 === func2); // true

const func3 = () => {
  console.log("doSomething");
};
const func4 = () => {
  console.log("doSomething");
};
console.log(func3 === func4); // false

前述のhandleClickが参照する関数も、Appコンポーネントが再描画される度に再生成されるため、等価ではない。

そのため、関数の内容が同じでもChildコンポーネントが再描画される。

この問題を解消するためには、useCallbackを利用して関数をメモ化する必要がある。

useCallback

メモ化されたコールバック関数を返すフック。

なぜ useCallback を利用するのか

React.memoと併用することで、コンポーネントの不要な再描画をスキップできるから。

より具体的に言えば、React.memoでメモ化したコンポーネントにuseCallbackでメモ化したコールバック関数を Props として渡すことで、コンポーネントの不要な再描画をスキップできるから。

useCallbackを単体で利用したり、メモ化したコールバック関数を Props として渡さないと意味はないので注意。

useCallback の構文

useCallback(コールバック関数, 依存配列);

依存配列とは、コールバック関数が依存している要素が格納された配列のこと。

例えば、countという変数をconsole.logで出力する関数をメモ化したい場合は以下のようになる。

const callback = useCallback(() => console.log(count), [count]);

依存している要素が更新されれば、関数が再生成される。

依存している要素がなければ、依存配列は空で OK。

const callback = useCallback(() => console.log("doSomething"), []);

useCallback の利用例

以下はメモ化したコールバック関数を渡し、コンポーネントは再描画をスキップしているデモ。
using-usecallback.gif
デモを見る

App.js
import React, { useState, useCallback } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <button onClick={props.handleClick}>Child</button>;
});

export default function App() {
  console.log("render App");

  const [count, setCount] = useState(0);
  // 関数をメモ化すれば、新しい handleClick と前回の handleClick は
  // 等価になる。そのため、Child コンポーネントは再描画されない。
  const handleClick = useCallback(() => {
    console.log("click");
  }, []);

  return (
    <>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
      <Child handleClick={handleClick} />
    </>
  );
}

useMemo

メモ化された値を返すフック。

コンポーネントの再描画時に値を再利用できる。

なぜ useMemo を利用するのか

値を再利用することで、値の不要な再計算をスキップできるから。

不要な再計算をスキップすることにより、パフォーマンスの向上が期待できるから。

useMemo の構文

useMemo(() => 値を計算するロジック, 依存配列);

依存配列とは、値を計算するロジックが依存している要素(値の計算に必要な要素)が格納された配列のこと。

例えば、countという変数の値を2倍にした値をメモ化したい場合は以下のようになる。

const result = useMemo(() => count * 2, [count]);

依存している要素が更新されれば、値が再計算される。

useMemo の利用例

useMemoを利用する場合と、しない場合では何が違うのか比較してみる。

useMemo を利用しない場合

以下はuseMemoを利用せず、不要な再計算が発生しているデモ
unused-usememo.gif
デモを見る

App.js
import React, { useState } from "react";

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // 引数の数値を2倍にして返す。
  // 不要なループを実行しているため計算にかなりの時間がかかる。
  const double = count => {
    let i = 0;
    while (i < 1000000000) i++;
    return count * 2;
  };

  // count2 を2倍にした値
  // double(count2) はコンポーネントが再描画される度に実行されるため、
  // count1 を更新してコンポーネントが再描画された時にも実行されてしまう。
  // そのため、count1 を更新してコンポーネントを再描画する時も時間がかかる。
  // count1 を更新しても doubledCount の値は変わらないため、count1 を更新した時に
  // double(count2) を実行する意味がない。したがって、不要な再計算が発生している状態である。
  // count1 が更新されてコンポーネントが再描画された時は double(count2) が実行されないようにしたい。
  const doubledCount = double(count2);

  return (
    <>
      <h2>Increment count1</h2>
      <p>Counter: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment count1</button>

      <h2>Increment count2</h2>
      <p>
        Counter: {count2}, {doubledCount}
      </p>
      <button onClick={() => setCount2(count2 + 1)}>Increment count2</button>
    </>
  );
}

count1を更新した時もdouble(count2)が実行されてしまうため、count1を更新してコンポーネントを再描画する時も時間がかかる。

useMemo を利用する場合

以下はuseMemoを利用し、不要な再計算をスキップするデモ。
using-usememo.gif
デモを見る

App.js
import React, { useState, useMemo } from "react";

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // 引数の数値を2倍にして返す。
  // 不要なループを実行しているため計算にかなりの時間がかかる。
  const double = count => {
    let i = 0;
    while (i < 1000000000) i++;
    return count * 2;
  };

  // count2 を2倍にした値をメモ化する。
  // 第2引数に count2 を渡しているため、count2 が更新された時だけ値が再計算される。
  // count1 が更新され、コンポーネントが再描画された時はメモ化した値を利用するため再計算されない。
  const doubledCount = useMemo(() => double(count2), [count2]);

  return (
    <>
      <h2>Increment(fast)</h2>
      <p>Counter: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment(fast)</button>

      <h2>Increment(slow)</h2>
      <p>
        Counter: {count2}, {doubledCount}
      </p>
      <button onClick={() => setCount2(count2 + 1)}>Increment(slow)</button>
    </>
  );
}

useMemoを利用して値をメモ化したため、count1を更新した時はdouble(count2)が実行されないようになった。

そのため、count1を更新した時のコンポーネントの再描画が高速になった。

依存配列は正しく指定する必要がある

useCallbackuseMemoの依存配列は正しく指定しないとバグの原因になる。

そのため、以下のコードは NG。

// 依存要素である count2 が依存配列にないため NG
const result = useMemo(() => count * count2, [count]);

// これが正しい
// const result = useMemo(() => count * count2, [count, count2]);

そのため、eslint-plugin-react-hooksなどを利用して、必ず Lint する。

使い所

パフォーマンスを計測し、ボトルネックになっている箇所に適用していくのが一番効果的。

とは言え、GitHub 上にあるコードや技術書を見てみると、ガンガン利用していた。

厳密な利用基準を設けるのも大変なので、それぞれの機能や役割をちゃんと理解しているのであれば、積極的に利用しても大きな問題はないと思った。

終わり

今回準備したデモは極端な例ですが、利用シーンによっては非常に有用な機能です。

状況に応じて利用していきましょう。

お知らせ

KDP(Kindle ダイレクト・パブリッシング)を利用して、技術書を出版しました。

どちらも 500 円(Kindle Unlimited だったら無料)です。

書籍の詳細は商品ページからご覧いただけます。

興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする 【React】

はじめに

React(v16.12.0)のReact.memouseCallbackuseMemoの基本的な使い方、使い所に関しての備忘録です。

  • 「React でのパフォーマンス最適化の手段を知りたい」
  • 「なぜReact.memouseCallbackuseMemoを利用するのかわからない」

といった人達向けに書いた記事です。

デモは CodeSandbox 上に置いてあります。編集して動作を確認してみると理解が深まると思います。

本記事で用いている用語

  • メモ化
  • 計算結果

メモ化

計算結果を保持し、それを再利用する手法のこと。

キャッシュのようなものだとイメージすれば良いと思う。

そのため、以下の言葉の意味は大体同じ。

  • 「メモ化された値」=「計算結果が保持された値」
  • 「メモ化する」=「計算結果を再利用できるように保持する」

メモ化によって都度計算する必要がなくなるため、パフォーマンスの向上が期待できる。

計算結果

以下のような計算の結果のこと。

// result は 1 + 2 の計算結果を格納している変数
const result = 1 + 2;

// result2 は [1, 2, 3, 4, 5].map(number => number * 2) の計算結果を格納している変数
const result2 = [1, 2, 3, 4, 5].map(number => number * 2);

// result3 は React.createElement("div", null, `Hello ${this.props.name}`) の計算結果を格納している変数
const result3 = React.createElement("div", null, `Hello ${this.props.name}`);

React におけるパフォーマンス最適化

React では、不要な再計算やコンポーネントの再レンダリングを抑えることが、パフォーマンス最適化の基本的な戦略となる。

それらを実現する手段としてReact.memouseCallbackuseMemoを利用する。

React 以外のパフォーマンスチューニングにも言えることだが、計測は必須。

無闇に利用してもパフォーマンスが向上するわけではなく、意味がない場合もあるため注意。

React.memo

コンポーネント(コンポーネントのレンダリング結果)をメモ化する React の API(メソッド)。

コンポーネントをメモ化することで、コンポーネントの再レンダリングをスキップできる。

なぜ React.memo を利用するのか

以下のようなコンポーネントの再レンダリングをスキップすることで、パフォーマンスの向上が期待できるから。

  • レンダリングコストが高いコンポーネント
  • 頻繁に再レンダリングされるコンポーネント内の子コンポーネント

通常のコンポーネントに対しては、わざわざReact.memoを利用する必要はない。

React.memo の構文

React.memo(コンポーネント);

例えば、Helloというコンポーネントをメモ化する場合は以下のようになる。

const Hello = React.memo(props => {
  return <h1>Hello {props.name}</h1>;
});

React.memoは Props の等価性(値が等価であること)をチェックして再レンダリングの判断をする。

新しく渡された Props と前回の Props を比較し、等価であれば再レンダリングをせずにメモ化したコンポーネントを再利用する。

そのため、上記のHelloコンポーネントの場合、props.nameが更新されない限りコンポーネントは再レンダリングされない。

React.memo の利用例

React.memoを利用する場合と、しない場合では何が違うのか比較してみる。

React.memo を利用しない場合

通常、コンポーネントの state が更新されると、そのコンポーネントは再レンダリングされる。

以下のデモのように親コンポーネントが再レンダリングされると、その子コンポーネントも常に再レンダリングされる。
react-memo.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = props => {
  console.log("render Child");
  return <p>Child: {props.count}</p>;
};

export default function App() {
  console.log("render App");
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  );
}

これが通常の挙動なので、この書き方が悪いわけではなく、問題もない。

コンポーネントの不要な再レンダリングでパフォーマンスの問題が発生した場合、React.memoの利用を検討する。

今回はChildコンポーネントが常に再レンダリングされても何も問題はないため、React.memoを利用する必要はない。

React.memo を利用する場合

以下はReact.memoを利用し、Childコンポーネントの再レンダリングをスキップしているデモ。
using-react-memo.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <p>Child: {props.count}</p>;
});

export default function App() {
  console.log("render App");

  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  );
}

count1を更新してAppコンポーネントを再レンダリングした時は、Childコンポーネントに渡される Props(count2)は更新されないため、再レンダリングはスキップされる。

Childコンポーネントに渡されるcount2が更新された時だけ、再レンダリングされるようになった。

レンダリングコストが高いコンポーネントをメモ化する

極端な例になるが、以下のデモのようにレンダリングコストが高いコンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
using-react-memo-02.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = React.memo(props => {
  let i = 0;
  while (i < 1000000000) i++;
  console.log("render Child");
  return <p>Child: {props.count}</p>;
});

export default function App() {
  console.log("render App");

  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  return (
    <>
      <button onClick={() => setCount1(count1 + 1)}>countup App count</button>
      <button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
      <p>App: {count1}</p>
      <Child count={count2} />
    </>
  );
}

頻繁に再レンダリングされるコンポーネント内の子コンポーネントをメモ化する

以下のデモのように、頻繁に再レンダリングされるコンポーネント内の子コンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
using-react-memo-03.gif
デモを見る

App.js
import React, { useState, useEffect, useRef } from "react";

const Child = React.memo(() => {
  console.log("render Child");
  return <p>Child</p>;
});

export default function App() {
  console.log("render App");

  const [timeLeft, setTimeLeft] = useState(100);
  const timerRef = useRef(null);
  const timeLeftRef = useRef(timeLeft);

  useEffect(() => {
    timeLeftRef.current = timeLeft;
  }, [timeLeft]);

  const tick = () => {
    if (timeLeftRef.current === 0) {
      clearInterval(timerRef.current);
      return;
    }
    setTimeLeft(prevTime => prevTime - 1);
  };

  const start = () => {
    timerRef.current = setInterval(tick, 10);
  };

  const reset = () => {
    clearInterval(timerRef.current);
    setTimeLeft(100);
  };

  return (
    <>
      <button onClick={start}>start</button>
      <button onClick={reset}>reset</button>
      <p>App: {timeLeft}</p>
      <Child />
    </>
  );
}

コールバック関数を Props として受け取ったコンポーネントは必ず再レンダリングされる

以下のデモのようにコールバック関数を受け取ったコンポーネントはReact.memoを利用しても必ず再レンダリングされる。
using-react-memo-04.gif
デモを見る

App.js
import React, { useState } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <button onClick={props.handleClick}>Child</button>;
});

export default function App() {
  console.log("render App");

  const [count, setCount] = useState(0);
  // 関数はコンポーネントが再レンダリングされる度に再生成されるため、
  // 関数の内容が同じでも、新しい handleClick と前回の handleClick は
  // 異なるオブジェクトなので、等価ではない。
  // そのため、コンポーネントが再レンダリングされる。
  const handleClick = () => {
    console.log("click");
  };

  return (
    <>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
      <Child handleClick={handleClick} />
    </>
  );
}

以下のように参照が異なる関数は別のオブジェクトとなる。

function doSomething() {
  console.log("doSomething");
}
const func1 = doSomething;
const func2 = doSomething;
console.log(doSomething === doSomething); // true
console.log(func1 === func2); // true

const func3 = () => {
  console.log("doSomething");
};
const func4 = () => {
  console.log("doSomething");
};
console.log(func3 === func4); // false

前述のhandleClickが参照する関数も、Appコンポーネントが再レンダリングされる度に再生成されるため、等価ではない。

そのため、関数の内容が同じでもChildコンポーネントが再レンダリングされる。

この問題を解消するためには、useCallbackを利用して関数をメモ化する必要がある。

useCallback

メモ化されたコールバック関数を返すフック。

なぜ useCallback を利用するのか

React.memoと併用することで、コンポーネントの不要な再レンダリングをスキップできるから。

より具体的に言えば、React.memoでメモ化したコンポーネントにuseCallbackでメモ化したコールバック関数を Props として渡すことで、コンポーネントの不要な再レンダリングをスキップできるから。

useCallbackを単体で利用したり、メモ化したコールバック関数を Props として渡さないと意味はないので注意。

useCallback の構文

useCallback(コールバック関数, 依存配列);

依存配列とは、コールバック関数が依存している要素が格納された配列のこと。

例えば、countという変数をconsole.logで出力する関数をメモ化したい場合は以下のようになる。

const callback = useCallback(() => console.log(count), [count]);

依存している要素が更新されれば、関数が再生成される。

依存している要素がなければ、依存配列は空で OK。

const callback = useCallback(() => console.log("doSomething"), []);

useCallback の利用例

以下はメモ化したコールバック関数を渡し、コンポーネントは再レンダリングをスキップしているデモ。
using-usecallback.gif
デモを見る

App.js
import React, { useState, useCallback } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <button onClick={props.handleClick}>Child</button>;
});

export default function App() {
  console.log("render App");

  const [count, setCount] = useState(0);
  // 関数をメモ化すれば、新しい handleClick と前回の handleClick は
  // 等価になる。そのため、Child コンポーネントは再レンダリングされない。
  const handleClick = useCallback(() => {
    console.log("click");
  }, []);

  return (
    <>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
      <Child handleClick={handleClick} />
    </>
  );
}

useMemo

メモ化された値を返すフック。

コンポーネントの再レンダリング時に値を再利用できる。

なぜ useMemo を利用するのか

値の不要な再計算をスキップすることで、パフォーマンスの向上が期待できるから。

useMemo の構文

useMemo(() => 値を計算するロジック, 依存配列);

依存配列とは、値を計算するロジックが依存している要素(値の計算に必要な要素)が格納された配列のこと。

例えば、countという変数の値を2倍にした値をメモ化したい場合は以下のようになる。

const result = useMemo(() => count * 2, [count]);

依存している要素が更新されれば、値が再計算される。

useMemo の利用例

useMemoを利用する場合と、しない場合では何が違うのか比較してみる。

useMemo を利用しない場合

以下はuseMemoを利用せず、不要な再計算が発生しているデモ
unused-usememo.gif
デモを見る

App.js
import React, { useState } from "react";

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // 引数の数値を2倍にして返す。
  // 不要なループを実行しているため計算にかなりの時間がかかる。
  const double = count => {
    let i = 0;
    while (i < 1000000000) i++;
    return count * 2;
  };

  // count2 を2倍にした値
  // double(count2) はコンポーネントが再レンダリングされる度に実行されるため、
  // count1 を更新してコンポーネントが再レンダリングされた時にも実行されてしまう。
  // そのため、count1 を更新してコンポーネントを再レンダリングする時も時間がかかる。
  // count1 を更新しても doubledCount の値は変わらないため、count1 を更新した時に
  // double(count2) を実行する意味がない。したがって、不要な再計算が発生している状態である。
  // count1 が更新されてコンポーネントが再レンダリングされた時は double(count2) が実行されないようにしたい。
  const doubledCount = double(count2);

  return (
    <>
      <h2>Increment count1</h2>
      <p>Counter: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment count1</button>

      <h2>Increment count2</h2>
      <p>
        Counter: {count2}, {doubledCount}
      </p>
      <button onClick={() => setCount2(count2 + 1)}>Increment count2</button>
    </>
  );
}

count1を更新した時もdouble(count2)が実行されてしまうため、count1を更新してコンポーネントを再レンダリングする時も時間がかかる。

useMemo を利用する場合

以下はuseMemoを利用し、不要な再計算をスキップするデモ。
using-usememo.gif
デモを見る

App.js
import React, { useState, useMemo } from "react";

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // 引数の数値を2倍にして返す。
  // 不要なループを実行しているため計算にかなりの時間がかかる。
  const double = count => {
    let i = 0;
    while (i < 1000000000) i++;
    return count * 2;
  };

  // count2 を2倍にした値をメモ化する。
  // 第2引数に count2 を渡しているため、count2 が更新された時だけ値が再計算される。
  // count1 が更新され、コンポーネントが再レンダリングされた時はメモ化した値を利用するため再計算されない。
  const doubledCount = useMemo(() => double(count2), [count2]);

  return (
    <>
      <h2>Increment(fast)</h2>
      <p>Counter: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment(fast)</button>

      <h2>Increment(slow)</h2>
      <p>
        Counter: {count2}, {doubledCount}
      </p>
      <button onClick={() => setCount2(count2 + 1)}>Increment(slow)</button>
    </>
  );
}

useMemoを利用して値をメモ化したため、count1を更新した時はdouble(count2)が実行されないようになった。

そのため、count1を更新した時のコンポーネントの再レンダリングが高速になった。

依存配列は正しく指定する必要がある

useCallbackuseMemoの依存配列は正しく指定しないとバグの原因になる。

そのため、以下のコードは NG。

// 依存要素である count2 が依存配列にないため NG
const result = useMemo(() => count * count2, [count]);

// これが正しい
// const result = useMemo(() => count * count2, [count, count2]);

そのため、eslint-plugin-react-hooksなどを利用して、必ず Lint する。

使い所

パフォーマンスを計測し、ボトルネックになっている箇所に適用していくのが一番効果的。

とは言え、GitHub 上にあるコードや技術書を見てみると、ガンガン利用していた。

厳密な利用基準を設けるのも大変なので、それぞれの機能や役割をちゃんと理解しているのであれば、積極的に利用しても大きな問題はないと思った。

終わり

今回準備したデモは極端な例ですが、利用シーンによっては非常に有用な機能です。

状況に応じて利用していきましょう。

お知らせ

KDP(Kindle ダイレクト・パブリッシング)を利用して、技術書を出版しました。

どちらも 500 円(Kindle Unlimited だったら無料)です。

書籍の詳細は商品ページからご覧いただけます。

興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む