Reactではstateが更新されると更新を行なったコンポーネントだけではなくその配下にあるすべてのコンポーネントが再描写(Re-render:再レンダリング)されます。子コンポーネントに渡すpropsが変更されているのであればpropsを受け取るコンポーネントの再描写は理解できると思うのですがpropsの更新が行われていなくても再描写されます。再描写を行うということはReact内でなにかしらの処理(UIとReactが持つstateを同期させる)が実行されるのでアプリケーションの動作の遅延につながる可能性があることはイメージしやすいかと思います。そのため必要ないコンポーネントの再描写であれば止めたいものです。ReactではuseCallback, useMemo, React.memoを利用することで必要のない再描写を行わせないようにすることができます。

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

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

memoはmemoizationで略で英語では計算の結果を保存するという意味があります。

React.memoではコンポーネント、useCallbackでは(コールバック)関数、useMemoでは関数による計算結果から戻される値や配列の値を保存することでコンポーネントの再描写を抑えることができます。

動作確認を行なったReactプロジェクトはcreate-react-appを利用して行っています。Reactのバージョン17を利用して文書を作成しています。Reactのバージョン18でも同様の動作となりますがデフォルトではコンソールに表示されるメッセージが同じ内容で2度表示されるのでコンソールに表示される内容が異なります。

利用するコード

動作確認を行うためのコードが必要となるため、今回はシンプルな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

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

記述したコードをブラウザで確認
記述したコードをブラウザで確認
ツリー構造
ツリー構造

React Developer Tools

React Developer ToolsのComponentsの設定で”Highlight updates when components render”を利用することでどのコンポーネントが再描写されているか確認することができます。

React Developer Toolsで再描写を確認
React Developer Toolsで再描写を確認

“Highlight updates when components render”を設定して再描写が行われると再描写したコンポーネントで四角で囲まれます。

再描写されたコンポーネント
再描写されたコンポーネント

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

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

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

描写の確認
Reactのバージョン17での描写の確認

Reactのバージョン18では下記のように表示されます。薄い文字を無視してくださいAppが1回、Todosが1回、TodoListが3回表示されています。

Reactのバージョンが18の場合
Reactのバージョンが18の場合

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

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

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

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

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

すべてのコンポーネント再描写
すべてのコンポーネント再描写

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

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

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

コンポーネントをReact.memoで包むことでpropsが更新されていない場合には再描写を抑えることができます。Todo.jsのコードに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の再描写もなくなります。

memoを利用
memoを利用

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

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

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

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

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

Todo追加後の再描写
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つだけ表示されます。

TodoListをmemo
TodoListをmemo

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

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

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

タスク完了機能の追加

useCallbackを利用する前にTodoを未完了から完了にするためのcompleteTodo関数をAppコンポーネントに追加します。completeTodo関数では引数でindexを受け取り、配列のindexを利用しtodos配列の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名の上に横棒が表示されます。これがTodoの完了を意味しています。

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

useCallbackの動作確認

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

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

描写の確認
描写の確認
propsに関数を追加
propsに関数を追加

完了機能の追加の前後で変更が行われたのはpropsであるcompleteTodo関数の追加です。文字を入力することはcompleteTodo関数と関連はありません。なぜpropsに関数を追加しただけで再描写を引き起こすのかという疑問が湧くかと思います。

原因はcompleteTodo関数はAppコンポーネントが再描写される度に新しい関数として再作成されるためApp.jsから渡されているpropsが更新されたことになり, Todo, TodoListの再描写が行われます。

再描写される度に新しい関数が再作成されるというのは下記のように処理の内容は同じでも参照先が異なるため異なる関数になるようにコンポーネントが再描写が行されると関数が新たに作成されるため異なる関数として認識されます。


const sum = (a, b) => a + b;
const sum2 = (a, b) => a + b;

console.log(sum === sum2); // false

input要素への入力によってAppコンポーネント再描写されてもcompleteTodoの新しい関数を作成させないためにuseCallbackを利用することができます。useCallbackでは新しい関数を作成する変わりに関数のインスタンスをキャッシュします。

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

useCallbackは下記のように設定を行います。useCallbackを利用するためにはuseStateと同様にimportを行っておく必要があります。React.memoはpropsを受け取る子コンポーネントで設定で行いましたが、useCallbackはpropsを渡す親コンポーネントで設定を行います。


import {useState, useCallback} from 'react'
//略
  const completeTodo = useCallback(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により再描写が抑えられる
useCallbackの設定
useCallbackの設定

Todoの追加を行うと依存関係に設定しているtodosが更新されるためcompleteTodo関数が再作成されててすべてのコンポーネントの再描写が行われます。先ほどcompleteTodoを追加する前にReact.memoを利用して動作確認した時はTodoを追加しても既存のTodoListは再描写されていませんでしたが、TodoListはpropsで新しく作成されたcompleteTodo関数を受け取っているので再描写が行われます。

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

useCallbackではタスク追加でTodoListも再描写
useCallbackではタスク追加でTodoListも再描写
Todoを追加した場合
Todoを追加した場合

依存関係を設定する配列を変更した場合

依存関係の配列に何も入れない場合、input要素から文字列を入力して”Enter”ボタンを押すとTodoは追加されます。しかし追加したTodoの完了ボタンをクリックすると追加したTodoが消え期待通りの動作は行われませんでした。開発サーバを起動したコンソールには”React Hook useCallback has a missing dependency: ‘todos’. Either include it or remove the dependency array react-hooks/exhaustive-deps”のWARNINGが表示されます。

useCallbackにcallback関数を設定するだけではなく正しく動作させるためには依存関係の配列に正しい値を設定する必要があることがわかります。

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回になっています。

タスクを追加した場合のコンソールのメッセージ
タスクを追加した場合のコンソールのメッセージ
再描写に注目してuseMemoの動作確認を行いましたがpropsとは関係なくコンポーネント内で複雑な計算を行う関数がある場合にはuseMemoを利用することで一度実行した結果をキャッシュし、2回目以降の複雑な計算の再実行を抑えることができます。
fukidashi

配列をpropsで渡した時の動作

stateや関数ではなくpropsにただの配列を渡した場合の再描写について確認を行います。App.jsにinput要素と新たに作成するDoneTodoコンポーネントを設定します。doneTodosを定義してDoneTodoコンポーネントにpropsで渡しています。


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

function App() {
  const [todo, setTodo] = useState('');
  const inputTodo = (e) => {
    setTodo(e.target.value);
  };
  const doneTodos = [
    {
      todo: 'Learn Laravel',
      isCompleted: true,
    },
    {
      todo: 'Learn React',
      isCompleted: true,
    },
  ];
  return (
    <div style={{ margin: '2em' }}>
      <label htmlFor="Todo">Todo</label>
      <input
        id="Todo"
        name="Todo"
        type="text"
        onChange={inputTodo}
        value={todo}
      />
      <p>{todo}</p>
      <DoneTodo doneTodos={doneTodos} />
    </div>
  );
}

export default App;

donTodoコンポーネントではpropsでdoneTodosを受け取りmap関数を利用して展開して表示しています。コンポーネントにはmemoを設定しています。


import { memo } from 'react';

const DoneTodo = ({ doneTodos }) => {
  console.log('Donetodo');
  return (
    <>
      <p>終了したTodo</p>
      <ul>
        {doneTodos.map((todo, index) => (
          <li key={index}>{todo.todo}</li>
        ))}
      </ul>
    </>
  );
};

export default memo(DoneTodo);

input要素に文字を入力してください。memoも設定しているのでdoneTodoコンポーネントの再描写は抑えられると思いますが実際は再描写されます。理由は関数をpropsで渡した時と非常に似ておりAppコンポーネントが再描写されるとdoneTodosの配列が再作成されることが原因です。doneTodosは配列ではないのでuseCallbackは利用することができません。ここでは計算は行なっていませんが値の場合にはuseMemoを利用することができます。


  const doneTodos = useMemo(() => {
    return [
      {
        todo: 'Learn vue.js',
        isCompleted: true,
      },
      {
        todo: 'Learn React',
        isCompleted: true,
      },
    ];
  }, []);

useMemoを利用することでApp.jsが再描写されても配列が再作成されることがなくなりdoneTodosのpropsを受け取るDoneTodoコンポーネントの再描写が行われなくなります。

useCallbackとuseEffect

ここまでの説明ではuseCallbackによって再描写を抑えるために必ずpropsを渡す子コンポーネントにはmemoを利用していました。useCallbackは必ずmemoが必要なのかという疑問を持った人もいるかもしれません。memoを利用しなくてもuseCallbackが利用できる一例としてuseEffectを使って動作確認を行います。

App.jsを下記のように記述します。useStateでnameとnationalityを定義してinputを設定します。ProfileコンポーネントにpropsでshowNationalityという関数を渡します。


import { useState } from 'react';
import './App.css';
import Profile from './components/Profile';

const App = () => {
  const [name, setName] = useState('');
  const [nationality, setNationality] = useState('');
  const showNationality = () =>  `私は${nationality}です`;
  return (
    <div>
      <h1>App</h1>
      <div className="app" >
        <label>名前</label>
        <input onChange={(e) => setName(e.target.value)} />
      </div>
      <div>
        <label>国籍</label>
        <input onChange={(e) => setNationality(e.target.value)} />
      </div>

      <Profile showNationality={showNationality} />
    </div>
  );
};

export default App;

Profileコンポーネントではpropsで受け取ったshowNationalityをuseEffectの中で実行します。


import { useEffect } from 'react';

const Profile = ({ showNationality }) => {
  console.log('再描写');
  useEffect(() => {
    const nationality = showNationality();
    console.log(nationality);
  }, [showNationality]);
  return <h1>Profileコンポーネント</h1>;
};

export default Profile;

memoもuseCallbackも何も利用していな状態でinput要素に入力を行うと入力の度にProfileコンポーネントは再描写され再描写されるとuseEffectも毎回実行されるのでコンソールには”私はXXです。”が表示されます。

showNationalityにuseCallbackを追加します。nationalityが更新された場合のみshowNationalityが再作成されます。


const showNationality = useCallback(
  () => `私は${nationality}です`,
  [nationality]
);

Profile.jsファイルの変更は行いません。input要素に文字を入力するとmemoを利用していないので毎回Profileコンポーネントは再描写されます。しかしuseEffectの中の処理については名前のinputに入力しても依存関係の配列に設定が行われていないため処理が行われなくなり国籍のinputに入力した時だけuseEffectが実行されるようになります。useEffectの依存関係の配列にshowNationalityのみ設定しているため名前のinputを入力してもshowNationalityは再作成されないため変更がないと判断されuseEffectが実行されません。このようにuseCallbackはmemoが必ず必要なわけではなくuseEffectの依存配列にも利用できることがわかりました。

Profileにmemoを追加すると国籍に入力した場合のみ再描写とuseEffectが実行されるようになり、名前のinputに入力しても再描写は行われなくなります。

カスタムHookでuseCallback

useCallbackの理解が進んだと思うので最後はさらに理解を深めるためにカスタムHookに利用されているuseCallbackについて確認していきます。

https://usehooks.com/というサイトにアクセスするとuseToggleというHookのコードが記述されています。このサンプルの例を利用してuseCallbackについて確認します。


import { useCallback, useState } from 'react';

// Usage
function App() {
  // Call the hook which returns, current value and the toggler function
  const [isTextChanged, setIsTextChanged] = useToggle();

  return (
    <button onClick={setIsTextChanged}>
      {isTextChanged ? 'Toggled' : 'Click to Toggle'}
    </button>
  );
}

// Hook
// Parameter is the boolean, with default "false" value
const useToggle = (initialState = false) => {
  // Initialize the state
  const [state, setState] = useState(initialState);

  // Define and memorize toggler function in case we pass down the comopnent,
  // This function change the boolean value to it's opposite value
  const toggle = useCallback(() => setState((state) => !state), []);

  return [state, toggle];
};

export default App;

ブラウザで確認すると”Click to Toggle”ボタンが表示され、ボタンをクリックするとボタンのテキストが”Toggled”ボタンに変更され、ボタンをクリックすると”Click to Toggle”と”Toggled”が切り替わります。

useToggleの説明
useToggleの説明

ここでは機能が重要ではなくtoggle関数の中身に注目してください。コードを見るとtoggle関数の中でuseCallbackが設定されていることがわかります。useCallbackが理解できていない人であればなぜuseCallbackが急に出てくるのかわからないと思いますがサンプルのコードには親切にもuseCallbackについての説明が記述されています。

”Define and memorize toggler function in case we pass down the comopnent,”

サンプルの例ではApp.jsでuseToggleを利用して子コンポーネントにpropsで渡すような処理は行っていないのでuseCallbackを設定しても意味はありませんが、useToggle関数の戻り値を子コンポーネントにpropsで渡す場合に備えてuseCallbackを設定してくれています。

サンプルコードとは異なり子コンポーネントにpropsで渡すコードを記述します。useToggle関数は変更がないので省略しています。


import { useCallback, useState } from 'react';

const Child = ({ setIsTextChanged }) => {
  console.log('再描写');
  return <button onClick={setIsTextChanged}>Toggle</button>;
};

function App() {
  const [isTextChanged, setIsTextChanged] = useToggle();
  const [name, setName] = useState('');

  return (
    <div>
      <h1>useToggle</h1>
      <div>
        <label>名前</label> <input onChange={(e) => setName(e.target.value)} />
      </div>
      <div>{isTextChanged ? 'Yes' : 'No'}</div>
      <Child setIsTextChanged={setIsTextChanged} />
    </div>
  );
}
//略

Childコンポーネントを追加してpropsでsetIsTextChanged関数を受け取りbutton要素のonClickに設定しています。setIsTextChanged関数はuseToggle HookでuseCallbackが設定されています。

現在の設定ではinput要素に文字を入力してもToggleボタンをクリックしてもChildコンポーネントは再描写されます。useCallbackを有効に活用するために再描写を抑えるためにChildコンポーネントにmemoを設定します。


import { useCallback, useState, memo } from 'react';

const Child = memo({ setIsTextChanged }) => {
  console.log('再描写');
  return <button onClick={setIsTextChanged}>Toggle</button>;
});

memoを設定した結果、input要素に文字を入力してもToggleボタンをクリックしてもChildコンポーネントの再描写は行われなくなります。useCallbackを有効に活用することができました。

Hookを作成する場合は戻す関数がpropsとして子コンポーネントに渡されることを想定してuseCallbackを設定しておくことで今後パフォーマンスに影響が出た場合に利用することができます。

今回のuseToggleではコードに説明が入っていたのでuseCallbackが設定されている理由を理解することができました。今後useCallbackに関する説明がコードに記述されていなくても何を意図しているかは今回の例で理解できたのではないでしょうか。これで急にuseCallbackが登場しても怖くないですね。

まとめ

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