Reactではstate、propsが更新されるとすべてのコンポーネントが再描写(Re-render)されます。パフォーマンスを向上させるため不必要な再描写を行わせたくない場合にuseCallback, useMemo, memoを利用することができます。

再描写とは何か。またどのタイミングで再描写の処理が行われているのかを確認しながら、useCallback, useMemo,React.memoの機能を説明していきます。

本文書ではどういった場合にuseCallbak, useMemo, memoの機能を利用すべきなのか等については説明を行っておりませんがどのような機能なのかを理解できるように説明を行なっています。

memoはmemorizationで略で英語では記憶するという意味があります。ここで説明する機能は記憶というよりもキャッシュという言葉のほうが適切かもしれません。

React.memoではコンポーネント、useCallbackでは(コールバック)関数、useMemoでは値をキャッシュすることでコンポーネントの再描写を抑えることができます。

利用するコード

動作確認を行うためのコードが必要となるため、今回はシンプルなTodoアプリケーションのコードを利用します。App.jsを含めて、Todo.js, TodoList.jsの3つのコンポーネントを利用します。Todo.jsとTodoList.jsファイルはcomponentsフォルダに保存します。

各コンポーネントの処理の先頭にメッセージを表示させるためconsole.logを追加します。描写が行われるとconsole.logで設定したメッセージがコンソール上に表示されます。このメッセージを確認することで再描写されているかどうかを判断しています。

利用するコードは下記の通りです。少し長く感じるかもしれませんがシンプルなコードです。3つのオブジェクトが入ったTodoリストのtodosをmap関数で展開しています。またinput要素でtodosに新規のTodoを追加できるようにしています。


import {useState} from 'react'
import Todo from './components/Todo';

function App() {

  console.log('App')

  const [todo, setTodo] = useState('');
  const [todos, setTodos] = useState([
    {
        todo: 'Learn vue.js',
        isCompleted: false
    },
    {
        todo: 'Learn React',
        isCompleted: false
    },
    {
        todo: 'Learn Laravel',
        isCompleted: false
    },
  ]);

  const inputTodo = (e) => {
    setTodo( e.target.value )
  }

  const addTodo = (e) => {
    e.preventDefault()
    setTodos(todos => [...todos,{todo:todo, isCompleted:false}])
    setTodo('')
  }

  return (
    <div style={{ margin: "2em"}}>
      <form onSubmit={ addTodo }>
        <input type="text" onChange={inputTodo} value={todo} />
      </form>
      <Todo todos={todos} />
    </div>
  );
}

export default App;

import TodoList from './TodoList'

function Todo({todos}) {
  console.log('Todo')
  return (
    <ul>
      {todos.map((todo,index) => 
        <TodoList todo={todo} key={index} /> 
      )}
    </ul>
  )
}

export default Todo

function TodoList({todo}) {
  console.log('TodoList')
  return (
    <li>
    { todo.todo }
    </li>
  )
}

export default TodoList

コード記述後にブラウザで確認すると以下の画面が表示されます。

記述したコードをブラウザで確認
記述したコードをブラウザで確認

再描写(re-render)されるとは?

state, propsが更新されるとすべてのコンポーネントが再描写するということがどういうものなのかイメージが湧いていない人もいるかもしれません。ブラウザを眺めていても再描写しているかどうかわからないのでブラウザのデベロッパーツールを開いてコンソールを確認してみましょう。

ブラウザでアクセスするとApp, Todo, TodoListが3回表示されます。TodoListが3になっているのはtodoリストに3つのTodoオブジェクトが入っているため3回TodoListの処理が行われているためです。TodoListに含まれるTodoの数によってTodoListが表示されるメッセージの回数は変わります。

描写の確認
描写の確認

App, Todo, TodoListがそれぞれ実行されなければブラウザ上にTodo一覧が表示されることはないので表示されるメッセージの数については疑問はないかと思います。

ここからはReactの再描写を知らない人にとっては驚きがあるかもしれません。

input要素に”L”と入力した瞬間にコンソールには下記のメッセージが表示されます。上から3行はページを開いた時のメッセージで先ほど確認したものです。4行目からのメッセージが”L”を入力した瞬間に追加表示されたメッセージです。

再描写の確認
再描写の確認

input要素に文字を追加しても文字を削除してもApp, Todo, TodoList 3回のメッセージ(3行)が表示されます。stateを更新することで再描写されると説明した通り、実際にコンソールのメッセージを目で見ることですべてのコンポーネントが再描写されるということを理解することができます。

input要素と関係ないTodo、TodoListまで再描写する必要なんてないのでは思われる人もいるかもしれません。そのためReactでは再描写を抑える方法がいくつか準備されています。まずReact.memoを利用してTodo, TodoListの再描写を抑える方法を確認していきます。

再描写を行わない設定を行った方が必ずしもパフォーマンスが優れているものではありません。これらの機能を使うためにも別途処理が必要となるためどちらが効率的なのかを確認する必要があります。

React.memoを使って再描写を抑える

コンポーネントをReact.memoで包むことでpropsに変更がない場合に再描写を抑えることができます。Todo.jsのコードにReact.memoをimportし、Todoをmemoで包んでいます。


import {memo} from 'react'; //追加
import TodoList from './TodoList.js'

function Todo({todos}) {
  console.log('Todo')
  return (
    <ul>
      {todos.map((todo,index) => 
        <TodoList todo={todo} key={index} />
      )}
    </ul>
  )
}

export default memo(Todo) //memoで包む

下記のように記述することも可能です。こちらの方がコンポーネントを包んでいるというのがわかりやすいかもしれません。


import { memo } from 'react';
import TodoList from './TodoList';

const Todo = memo(({ todos }) => {
  console.log('Todo');
  return (
    <ul>
      {todos.map((todo, index) => (
        <TodoList todo={todo} key={index} />
      ))}
    </ul>
  );
});

export default Todo;

この状態でinput要素に文字”L”を入力してください。先ほどは文字を入力するとTodo, TodoListがコンソールに表示されていましたがmemoでコンポーネントを包むとApp以外のメッセージは表示されなくなります。ここでも最初の3行は表示直後のもので4行目AppのみがLを入力した時のメッセージです。このことからmemoの機能によりTodo, TodoListの再描写が抑えられていることを理解することができます。

memoのより再描写が抑えられる
memoにより再描写が抑えられる

Todoコンポーネントに対してmemoの設定を行うとその中のコンポーネントであるTodoListの再描写もなくなります。

この状態でinputに入力した情報を”Enter”キーを押してtodoリストに追加します。ブラウザ上のTodoリストには新たに”Learn React.memo”のタスクが追加されています。

タスクを追加
タスクを追加

コンソールのメッセージは下記のように表示されます。

タスク追加時のメッセージ
タスク追加時のメッセージ

4行目の”16 App”というのはinpu要素に入力した文字数です。Enterキーを押すとTodoコンポーネントに渡しているtodosの中身が変わるので(タスクを一つ追加)App, Todo, TodoListが再描写されます。”3 TodoList”とは別に追加で表示されているTodoListは追加したTodoに関するメッセージです。

propsのtodosが更新されたのでTodoが再描写されるのは理解できます。既存のTodoListには追加のTodoの影響がないので再描写させたくない場合はTodoListをmemoで包みます。


import {memo} from 'react';
function TodoList({todo}) {
  console.log('TodoList')
  return (
    <li>
    { todo.todo }
    </li>
  )
}

export default memo(TodoList)

設定後にブラウザをリロードして再度”Learn React.memo”のTodoを追加します。

TodoListの再描写を抑える
TodoListの再描写を抑える

TodoListをmemoで包む前とは異なり、既存の3つのTodoListの再描写が抑えられることが確認でき追加したTodoのメッセージであるTodoListが1つだけ表示されます。

ここまでの動作確認でmemoの設定方法とmemoを設定したコンポーネントの再描写を抑えられることを理解することができました。

useCallbackを使って再描写を抑える

先ほどはmemoを利用することでpropsに影響がないコンポーネントの再描写を抑えることができました。次はuseCallbackを利用してコンポーネントの再描写を抑える方法を確認します。memoではコンポーネントに対して設定を行っていましたが、useCallbackはcallbackという名前が含まれている通りコールバック関数に対して設定をおこないます。

タスク完了機能の追加

useCallbackを利用する前にTodoを未完了から完了にするための更新機能を追加します。配列のindexを利用し配列のindexと同じ番号を持つTodoのisCompletedプロパティをfalseからtrueに変更します。(trueであればfalseに)


const completeTodo = index => {
  console.log(index)
  let newTodos = todos.map((todo,todoIndex) => {
    if(todoIndex  === index){
      todo.isCompleted = !todo.isCompleted;
    }
    return todo;
    })
  setTodos(newTodos);
}

completeTodo関数を子コンポーネントであるTodoListで実行させるため、propsを利用して子コンポーネントに関数を渡します。


<div style={{ margin: "2em"}}>
  <form onSubmit={ addTodo }>
    <input type="text" onChange={inputTodo} value={todo} />
  </form>
  <Todo todos={todos} completeTodo={completeTodo}/>
</div>

App.jsとTodoList.jsの間にもTodoコンポーネントがありますが、Todoコンポーネントではpropsで受け取ったcompleteTodoをそのままpropsでTodoListコンポーネントに渡します。indexもpropsで渡します。


import {memo} from 'react';
import TodoList from './TodoList.js'

function Todo({todos,completeTodo}) {
  console.log('Todo')
  return (
    <ul>
      {todos.map((todo,index) => 
        <TodoList 
          todo={todo} 
          completeTodo={completeTodo}
          index={index} 
          key={index} 
        /> 
      )}
    </ul>
  )
}

export default memo(Todo)

TodoListコンポーネントでは、ボタンを追加しonClickイベントでcompleteTodoを実行します。またstyleを使ってtodoのisCompletedプロパティがtrueの場合は横棒が文字の上に引かれるようにtextDecorationLineを設定しています。


import {memo} from 'react';
function TodoList({todo,completeTodo,index}) {
  console.log('TodoList')
  return (
    <li 
      style={ todo.isCompleted === true ? {textDecorationLine: 'line-through'}:{}}
    >
      { todo.todo }
    <button onClick={() => completeTodo(index)}>完了</button>
    </li>
  )
}

export default memo(TodoList)

ブラウザで確認すると”完了”ボタンをクリックするとボタンを押したTodo名の上に横棒が表示されます。

タスク完了機能の動作確認
タスク完了機能の動作確認

useCallbackの動作確認

コードはmemoの動作確認を利用したものに完了機能を追加しただけなのでmemoは設定されたままです。

input要素に文字を入力すると完了機能の追加前までの状態ではmemoの設定によりAppのみが再描写されていましたが、文字を入力する度にApp, Todo, TodoListが再描写されるようになっています。

描写の確認
描写の確認

完了機能の追加の前後で変更が行われたのはpropsであるcompleteTodoの追加です。completeTodoはAppが再描写される度に新しい関数を作成しているためApp.jsから渡されているpropsが更新されたことになり, Todo, TodoListの再描写が行われます。stateだけではなくpropsも再描写に影響を与えることがわかります。

Appが再描写されてもcomplateTodoが新しい関数を作成させないためにuseCallbackを利用することができます。useCallbackでは新しい関数を作成する変わりに関数のインスタンスをキャッシュさせます。

useCallbackでは依存関係のある変数を設定することができるのでここではtodosを設定します。todosに変更が加わるとuseCallbackが実行され新しい関数が作成されます。

useCallbackは下記のように設定をおこないます。useCallbackを利用するためにはuseStataと同様にimportを行っておく必要があります。配列で依存関係のあるtodosを設定しています。


import {useState, useCallback} from 'react'
//略
  const completeTodo = useCallback(index => {
    console.log(index)
    let newTodos = todos.map((todo,todoIndex) => {
      if(todoIndex  === index){
        todo.isCompleted = !todo.isCompleted;
      }
      return todo;
      })
    setTodos(newTodos);
  },[todos]);

ブラウザのコンソールで動作を確認してみましょう。useCallbackを設定することでinput要素に文字を入力してもTodo, TodoListの再描写が抑えらることがわかります。

memoのより再描写が抑えられる
useCallbackにより再描写が抑えられる

Todoの追加を行うと依存関係に設定しているtodosが更新されているためuseCallback内の処理が行われます。React.memoの場合は既存のTodoListは再描写されていませんでしたが、TodoListはpropsで新しく作成されたcompleteTodoを受け取っているので再描写されます。

依存関係を設定する配列に何も入れない場合はタスクを追加してもcompleteTodoは新しく作成されません。completeTodoの更新が行われないため追加したタスクが描写されますが既存タスクは再描写は行われません。またtodoを配列に設定すると文字列を入力する度にTodo, TodoListの再描写が行われます。

“Learn useCallback”のタスクを追加した場合にコンソールには下記が表示されます。

useCallbackではタスク追加でTodoListも再描写
useCallbackではタスク追加でTodoListも再描写

useCallbackの動作確認を通して関数はコンポーネントの再描写の度に新しい関数が作成されることがわかり、新しい関数の再作成を抑えるためにuseCallbackを利用できることがわかりました。

useMemoを使って再描写を抑える

ここまでの動作確認でmemoとuseCallbackを使うことで再描写を抑えることができることを確認しました。最後はuseMemoを利用してコンポーネントの再描写を抑える方法を確認します。memoではコンポーネントに対して設定を行い、useCallbackでは関数に対して設定を行っていましたがuseMemoでは値に対して設定をおこないます。

完了したタスクのみ取得

Todoコンポーネントには完了、未完了に関わらずすべてのTodoをTodoコンポーネントにpropsとして渡していましたが、新たにnotCompleteTodosを追加し、未完了のタスクの一覧のみTodoコンポーネントに渡します。notCompleteTodosが実行されるとコンソールにメッセージが表示されるように設定を行っています。


const notCompleteTodos = todos.filter(todo => {
  console.log('notComplete')
  return todo.isCompleted === false;
})
//略
<Todo todos={notCompleteTodos} completeTodo={completeTodo} />

useMemoの動作確認

useMemoを設定する前の状態でinput要素に文字を入力するとどのようなメッセージが表示されるか確認します。

useMemoを設定する前のメッセージ
useMemoを設定する前のメッセージ

ブラウザでアクセスするとnotCompleteTodosの処理が行われ、todosに3つのTodoが入っているのでfilter関数によりnotCompleteが3回表示されます。その後input要素に文字”L”を入力すると再描写によりnotCompleteTodosの処理が行われ、処理結果がtodosのpropsでTodoコンポーネントに渡されるのでTodoが再描写されます。既存のtodoの内容が更新されたわけではないのでTodoListの再描写は行われません。TodoListからmemoを外すとTodoListの再描写は行われます。

input要素への文字の入力ではnotCompletedTodosの値の更新は行われないのためnotCompoletedTodosの再計算を行う必要はありません。再計算を行わないようにuseMemoを設定します。

useCallbackと同様に依存関係のある変数を配列で指定します。ここでは配列にtodosを設定し、todosに変更があった場合のみuseMemo内の値の再計算をおこないます。


import {useState, useCallback, useMemo} from 'react'
//略
const notCompleteTodos = useMemo(() => todos.filter(todo => {
  console.log('filter')
  return todo.isCompleted === false
}),[todos])

設定後はinput要素に文字を入力してもnotCompletedTodosの値の再計算は行われないためTodoの再描写が行われることはありません。”Learn useMemo”と入力しているのでAppは13回再描写されています。

useMemoにより再計算が行われない
useMemoにより再計算が行われない

input要素に入力後にTodoを追加するとコンソール上のメッセージは下記の通りとなります。filterはfilter関数内で処理された回数なので一つTodoが増えたことによりfilterメッセージがTodo追加時に4回になっています。

タスクを追加した場合のコンソールのメッセージ
タスクを追加した場合のコンソールのメッセージ

memo, useCallback, useMemoと3つの方法で再描写を抑える方法を確認しました。またReactの再描写とはどのようなことなのかまたどのようなタイミングで再描写が行われているのかがわかりました。使い方が理解できたので次はどのような時にどの機能を利用するかを理解していく必要があります。