- 投稿日:2020-05-25T22:42:53+09:00
[React][Reactチュートリアル]ふりがな
はじめに
React.jsを知るためにReactチュートリアルを使用しました。
その際に書いたコードに自分なりのふりがなをつけたため、記事として残したいと思います。index.jsimport 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; }
- 投稿日:2020-05-25T21:12:47+09:00
2. State 管理を Redux に移行する - Redux よくわからんので Todo つくる
はじめに
前回は、React のみでこのようななんでもない Todo アプリを作成しました
はい
なんでもないですね今回は当初の目的である、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.tsconst ADD_TODO = "ADD_TODO" as const; const TOGGLE_COMPLETED = "TOGGLE_COMPLETED" as const; const DELETE_TODO = "DELETE_TODO" as const;src/actions/filter/index.tsconst SET_FILTER = "SET_FILTER" as const;あとは必要なデータを引数に受け取り、いい感じに加工して Action を作ります
若干 JSON API 意識で、必要なデータは全てdata
プロパティ内に入れていますsrc/actions/todo/index.tsconst 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.tsconst 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.tstype AddTodoAction = ReturnType<typeof addTodo>; type ToggleCompletedAction = ReturnType<typeof toggleCompleted>; type DeleteTodoAction = ReturnType<typeof deleteTodo>; type TodoActions = | AddTodoAction | ToggleCompletedAction | DeleteTodoAction;src/actions/filter/types.tstype SetFilterAction = ReturnType<typeof setFilter>;Reducer
引数の
state, action
の型定義のため、Redux.Reducer<State, Action>
を使用します初期化時には
state
にundefined
が渡されるため、初期値を設定し
Action type で識別し、新しい State を返しますdefault case では引数
action
を never 型に割り当てることで、絞り込みの漏れが無いようにしています
参考: TypeScript 2.0のneverでTagged union typesの絞込を漏れ無くチェックするState の生成コードは前回と変わりないですね
src/store/reducer/todo/index.tsconst 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.tsconst 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.tscombineReducers({ todoReducer, filterReducer });Store
Reducer を createStore に渡せば Store の完成です
src/store/index.tscreateStore(reducer);型定義
前回の State 型と
Store 全体の型を定義しますsrc/store/types.tstype 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.tsximport { 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.tsimport { 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.tsimport { 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
- 投稿日:2020-05-25T18:08:37+09:00
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.
- 投稿日:2020-05-25T15:46:24+09:00
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>実際にブラウザで表示するとどうなるか!?
Reactから「contentEditableはあなたの責任で使いなさいよ」という警告メッセージが出ていました。
はーい、がんばりまーす。
って言うほど頑張れないので、メッセージ出さない方法ないかなぁ?
と調べた結果、react-contenteditableというものを見つけたので、使ってみたいと思います。react-contenteditableの追加
以下のとおり、yarnで追加できました。
shellbundle exec yarn add react-contenteditable
react-contenteditableを使ってみる。
インストールができたので、ソースコードに追加してみます。
対象は、先ほどのSticky.jsです。ソースコードの変更
app/javascript/components/Sticky.jsimport 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 動作確認
おぉ!編集できる!!
ならば、この値を取得して保存する流れを作ってみます。CotentEditableで作成した要素から値を取得する。
reactで編集可能な要素の値を取得するには、onChangeを使ってstateに値を保存するなどの方法が考えられますが、ここではrefを使った方法を使ってみます。
ContentEditableコンポーネントでは、innerRefというプロパティを使うことで実現します。具体的には以下のようなソースコードとなります。(変更箇所に1〜5の番号をふってあります。)
app/javascript/components/Sticky.jsimport 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; }試してみます。
タイトルを編集(hogegeってしたり、updatedってしたり)して、"save"を押してみると。
おぉ、ログが出た。
実際にDBに保存させてみる。
編集した値が取得できることが分かったので、実際にDBに反映する流れを作ってみます。
シーケンス図で考える。
Railsで作ってるAPIにて
APIの処理イメージ@task = Task.find(params[:task][:id]); @task.update(params[:task]);みたいな感じになれば良いわけですが。
誰がこのAPIを呼ぶ?
みたいなことを考えねばなりません。で、以前作っているWhiteBoard.jsですでにAPI呼び出しを行っているので、
こいつにまとめてしまおう。
ちょっと複雑になるので、シーケンス図を起こしてみました。
図にするとわかりやすいですね。
最終的に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); endViewを作る
次に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.rbnamespace :api do put 'tasks/update' endとりあえずテスト
ブラウザでいちいち動作確認するのめんどいので、テストコードを追加しときます。
エラー系は、とりあえず後で。。test/controllers/api/tasks_controller_test.rbtest "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.jsimport 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.rbtest "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まとめ
- Reactでレンダリングした要素でcontentEditable=trueを使いたい場合は、react-contenteditableを使うと良い。
- しかし、今回の場合はinputタグでスタイル指定してそれっぽく見せたほうが良かったのではないかと。
- ContentEditableで描画した部分も普通にCapybaraでテストできる。
- 投稿日:2020-05-25T14:42:08+09:00
華麗なる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-routesReact Hydration
Gatsbyは、HTMLを静的に生成する静的サイトジェネレーターとしての機能、それに加えて、
生成したHTMLを、React hydration
を通してクライアントサイドで拡張し、アプリのような振る舞いを持たせます。上記の機能とGatsbyに付属している@reach/routerを使用し、client only routes,つまり静的ページとしては吐き出さないページを作ることができます。
認証機能においては、Gatsbyが生成した静的HTMLはファイルサーバ上にあるので、制御が不可能です。(ユーザーが直接URLを入力するとアクセスできてしまう)
なので、client only routesを使用することでユーザーをルーティングさせ、アクセスを制限することが必要となります。src/pages/app.jsimport 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 Appsrc/pages/Auth.jsimport React from "react" export default function Auth() { return ( <div>認証ページ</div> ) }以上のコーディングで、
localhost:8000/app
にアクセスすると、認証ページ、と記述されたページを出すことができます。またGatsbyではビルドがNode.jsで実行される関係でビルド時に
localStorage
やwindow
を使うことができません。しかし、外部認証サービスなどの中にはlocalStorageやwindowといったものにアクセスするものもあります。なので、ビルド中に不具合を起こさないため、該当コードをラッピングする必要があります。
import app from "firebase/app" if (typeof window !== 'undefined'){ app.initializeApp(config) }onCreatePage
gatsby-node.jsを編集して、/app/が制限された区画であることを定義して、必要に応じてページを作成するようにします。
onCreatePage
は、全てのページが作成された後に呼ばれます。
matchPath
で指定された部分は、build時に生成しないようになります。gatsby-node.jsexports.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.jsexport 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.jsimport 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 AppPrivateRouteは下記のようなHOCとなっています。
src/components/PrivateRouteimport 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.jsimport 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.jsimport 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は見ることができません。
- 投稿日:2020-05-25T07:57:42+09:00
【初心者向け】ReactでToDoアプリを作ってみた
概要
今回はアプリ制作の登竜門とも言える、
ToDo
アプリを作成してみました。
実はこれ、単純に見えて意外と複雑!
でもReactでstate・propsなどの流れを確認するにはちょうどいいレベルですね。完成形はこちら
搭載機能
開発環境
・macOS Catalina ver.10.15.4
・Editor:VScode
・node.js(create-react-app)全体図
主に使うやつだけ載せました!
ざっとですが、流れとしてはindex.html←index.js←App.js・index.css←componentsって感じです。
ここの位置関係をちゃんと把握しておくと、後々楽になってきます。解説
今回はUIのパーツごと・ファイルごとにコードの成り立ちを解説している形になっています。
ですので、実際に作っていく流れとは違うとは思います!ご了承を。1.TodoHeader.jsx
2.TodoForm.jsx
3.TodoList.jsx
4.App.js
5.index.css1.TodoHeader.jsx
これはリストの最上部を描画しているコンポーネントです。
ここで実装されている主な機能は
・複数で削除が可能、Alert機能
・Check/総数 でカウント
ですね。
全体のコードはこちら!JSX.jsimport 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
これはリストの中部を描画しているコンポーネントです。
ここで実装されている主な機能は
・チェックをつけたら斜線が入る
・何もToDoがない時はPerfectを表示
・単独で削除が可能、Alert機能
ですね。
全体のコードはこちら!JSX.jsimport 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 ボタンで追加が可能
ですね。
全体のコードはこちら!JSX.jsimport 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.jsimport 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.jsbody { 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; }終わり
少し後半雑になってしまったのは自分自身、完璧に理解できていないからですね
もっと噛み砕いて説明できるように精進したいなと思います。
- 投稿日:2020-05-25T00:01:22+09:00
React の React.memo、useCallback、useMemo の使い方、使い所を理解してパフォーマンス最適化をする
はじめに
React(v16.12.0)の
React.memo
、useCallback
、useMemo
の基本的な使い方、使い所に関しての備忘録です。デモは 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.memo
、useCallback
、useMemo
を利用する。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 が更新されると、そのコンポーネントは再描画される。
以下のデモのように親コンポーネントが再描画されると、その子コンポーネントも常に再描画される。
デモを見るApp.jsimport 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
コンポーネントの再描画をスキップしているデモ。
デモを見るApp.jsimport 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
が更新された時だけ、再描画されるようになった。描画コストが高いコンポーネントをメモ化する
極端な例になるが、以下のデモのように描画コストが高いコンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
デモを見るApp.jsimport 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} /> </> ); }頻繁に再描画されるコンポーネント内の子コンポーネントをメモ化する
以下のデモのように、頻繁に再描画されるコンポーネント内の子コンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
デモを見るApp.jsimport 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
を利用しても必ず再描画される。
デモを見るApp.jsimport 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 の利用例
以下はメモ化したコールバック関数を渡し、コンポーネントは再描画をスキップしているデモ。
デモを見るApp.jsimport 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
を利用せず、不要な再計算が発生しているデモ
デモを見るApp.jsimport 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
を利用し、不要な再計算をスキップするデモ。
デモを見るApp.jsimport 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
を更新した時のコンポーネントの再描画が高速になった。依存配列は正しく指定する必要がある
useCallback
とuseMemo
の依存配列は正しく指定しないとバグの原因になる。そのため、以下のコードは 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 だったら無料)です。
書籍の詳細は商品ページからご覧いただけます。
興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。
- 投稿日:2020-05-25T00:01:22+09:00
React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする
はじめに
React(v16.12.0)の
React.memo
、useCallback
、useMemo
の基本的な使い方、使い所に関しての備忘録です。
- 「React でのパフォーマンス最適化の手段を知りたい」
- 「なぜ
React.memo
、useCallback
、useMemo
を利用するのかわからない」といった人達向けに書いた記事です。
デモは 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.memo
、useCallback
、useMemo
を利用する。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 が更新されると、そのコンポーネントは再レンダリングされる。
以下のデモのように親コンポーネントが再レンダリングされると、その子コンポーネントも常に再レンダリングされる。
デモを見るApp.jsimport 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
コンポーネントの再レンダリングをスキップしているデモ。
デモを見るApp.jsimport 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
が更新された時だけ、再レンダリングされるようになった。レンダリングコストが高いコンポーネントをメモ化する
極端な例になるが、以下のデモのようにレンダリングコストが高いコンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
デモを見るApp.jsimport 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} /> </> ); }頻繁に再レンダリングされるコンポーネント内の子コンポーネントをメモ化する
以下のデモのように、頻繁に再レンダリングされるコンポーネント内の子コンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
デモを見るApp.jsimport 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
を利用しても必ず再レンダリングされる。
デモを見るApp.jsimport 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 の利用例
以下はメモ化したコールバック関数を渡し、コンポーネントは再レンダリングをスキップしているデモ。
デモを見るApp.jsimport 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 の注意点
前述の通り、
useCallback
はReact.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
を利用せず、不要な再計算が発生しているデモ
デモを見るApp.jsimport 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
を利用し、不要な再計算をスキップするデモ。
デモを見るApp.jsimport 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
のようにコンポーネントの再レンダリングをスキップできる。以下はコンポーネントをメモ化して、不要な再レンダリングをスキップしているデモ。
デモを見るApp.jsimport 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
を利用しても意味がないので注意。
デモを見るApp.jsimport 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> </> ); }依存配列は正しく指定する必要がある
useCallback
とuseMemo
の依存配列は正しく指定しないとバグの原因になる。そのため、以下のコードは 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 だったら無料)です。
書籍の詳細は商品ページからご覧いただけます。
興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。
- 投稿日:2020-05-25T00:01:22+09:00
React の React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする
はじめに
React(v16.12.0)の
React.memo
、useCallback
、useMemo
の基本的な使い方、使い所に関しての備忘録です。デモは 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.memo
、useCallback
、useMemo
を利用する。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 が更新されると、そのコンポーネントは再描画される。
以下のデモのように親コンポーネントが再描画されると、その子コンポーネントも常に再描画される。
デモを見るApp.jsimport 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
コンポーネントの再描画をスキップしているデモ。
デモを見るApp.jsimport 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
が更新された時だけ、再描画されるようになった。描画コストが高いコンポーネントをメモ化する
極端な例になるが、以下のデモのように描画コストが高いコンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
デモを見るApp.jsimport 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} /> </> ); }頻繁に再描画されるコンポーネント内の子コンポーネントをメモ化する
以下のデモのように、頻繁に再描画されるコンポーネント内の子コンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
デモを見るApp.jsimport 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
を利用しても必ず再描画される。
デモを見るApp.jsimport 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 の利用例
以下はメモ化したコールバック関数を渡し、コンポーネントは再描画をスキップしているデモ。
デモを見るApp.jsimport 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
を利用せず、不要な再計算が発生しているデモ
デモを見るApp.jsimport 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
を利用し、不要な再計算をスキップするデモ。
デモを見るApp.jsimport 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
を更新した時のコンポーネントの再描画が高速になった。依存配列は正しく指定する必要がある
useCallback
とuseMemo
の依存配列は正しく指定しないとバグの原因になる。そのため、以下のコードは 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 だったら無料)です。
書籍の詳細は商品ページからご覧いただけます。
興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。
- 投稿日:2020-05-25T00:01:22+09:00
React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする【React】
はじめに
React(v16.12.0)の
React.memo
、useCallback
、useMemo
の基本的な使い方、使い所に関しての備忘録です。デモは 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.memo
、useCallback
、useMemo
を利用する。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 が更新されると、そのコンポーネントは再描画される。
以下のデモのように親コンポーネントが再描画されると、その子コンポーネントも常に再描画される。
デモを見るApp.jsimport 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
コンポーネントの再描画をスキップしているデモ。
デモを見るApp.jsimport 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
が更新された時だけ、再描画されるようになった。描画コストが高いコンポーネントをメモ化する
極端な例になるが、以下のデモのように描画コストが高いコンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
デモを見るApp.jsimport 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} /> </> ); }頻繁に再描画されるコンポーネント内の子コンポーネントをメモ化する
以下のデモのように、頻繁に再描画されるコンポーネント内の子コンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
デモを見るApp.jsimport 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
を利用しても必ず再描画される。
デモを見るApp.jsimport 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 の利用例
以下はメモ化したコールバック関数を渡し、コンポーネントは再描画をスキップしているデモ。
デモを見るApp.jsimport 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
を利用せず、不要な再計算が発生しているデモ
デモを見るApp.jsimport 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
を利用し、不要な再計算をスキップするデモ。
デモを見るApp.jsimport 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
を更新した時のコンポーネントの再描画が高速になった。依存配列は正しく指定する必要がある
useCallback
とuseMemo
の依存配列は正しく指定しないとバグの原因になる。そのため、以下のコードは 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 だったら無料)です。
書籍の詳細は商品ページからご覧いただけます。
興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。
- 投稿日:2020-05-25T00:01:22+09:00
React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする 【React】
はじめに
React(v16.12.0)の
React.memo
、useCallback
、useMemo
の基本的な使い方、使い所に関しての備忘録です。
- 「React でのパフォーマンス最適化の手段を知りたい」
- 「なぜ
React.memo
、useCallback
、useMemo
を利用するのかわからない」といった人達向けに書いた記事です。
デモは 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.memo
、useCallback
、useMemo
を利用する。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 が更新されると、そのコンポーネントは再レンダリングされる。
以下のデモのように親コンポーネントが再レンダリングされると、その子コンポーネントも常に再レンダリングされる。
デモを見るApp.jsimport 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
コンポーネントの再レンダリングをスキップしているデモ。
デモを見るApp.jsimport 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
が更新された時だけ、再レンダリングされるようになった。レンダリングコストが高いコンポーネントをメモ化する
極端な例になるが、以下のデモのようにレンダリングコストが高いコンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
デモを見るApp.jsimport 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} /> </> ); }頻繁に再レンダリングされるコンポーネント内の子コンポーネントをメモ化する
以下のデモのように、頻繁に再レンダリングされるコンポーネント内の子コンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
デモを見るApp.jsimport 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
を利用しても必ず再レンダリングされる。
デモを見るApp.jsimport 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 の利用例
以下はメモ化したコールバック関数を渡し、コンポーネントは再レンダリングをスキップしているデモ。
デモを見るApp.jsimport 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
を利用せず、不要な再計算が発生しているデモ
デモを見るApp.jsimport 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
を利用し、不要な再計算をスキップするデモ。
デモを見るApp.jsimport 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
を更新した時のコンポーネントの再レンダリングが高速になった。依存配列は正しく指定する必要がある
useCallback
とuseMemo
の依存配列は正しく指定しないとバグの原因になる。そのため、以下のコードは 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 だったら無料)です。
書籍の詳細は商品ページからご覧いただけます。
興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。