BLOG

ブログ

【React】ミュータブルとイミュータブルについて

こんにちは、高田朱美です!

Reactを学習中、ミュータブルイミュータブルなど
聞いたこともない言葉がでてきまして、
気になったので調べてみました!

経緯

ReactでTODOアプリを作成中、useStateでTODOリストの更新をしようと思いググっていたのですが、調べたサイトのほぼ全てで、
スプレッド構文や、Array.slice()でstateをコピーしているのが目に付きました

import React, { useState } from "react";

const TodoList: React.FC = () => {
  // TODOリストの型
  type Todo = {
    TodoText: string;
  };

  // 入力値の状態管理
  const [text, setText] = useState<string>("");
  // TODOリストの状態管理
  const [todoList, setTodoList] = useState<Todo[]>([]);

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setText(event.target.value);
  };

  // TODOリストを更新
  const handleSubmit = () => {
    // 新しく入力されたTODOを作成
    const newTodo: Todo = {
      TodoText: text,
    };

	// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!ここ!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    setTodoList([...todoList, newTodo]);
  };

  return (
    <>
      <form onSubmit={(event) => handleSubmit()}>
        <input
          type="text"
          value={text}
          onChange={(event) => handleChange(event)}
        />
        <button type="submit">追加ボタン</button>
      </form>
    </>
  );
};

なぜそんなことをしなければならないのか疑問に思ったので調べてみました。

イミュータブル?

Reactで配列やオブジェクトのstateを操作する方法は以下の2つが多かったです。
①slice()メソッドを使用して、新しい変数に現状のstate配列やオブジェクトを入れる。

const [todo, setTodo] = useState([])

const newTodo = todo.slice()
newTodo.push(〇〇)
setTodo(newTodo)

②スプレッド構文を用いる。

const [todo, setTodo] = useState([])

const newTodo = "aaa"
setTodo([...todo, newTodo])

他にもやり方はあると思いますが、もともとのstateを直接変更していない点は、
両方とも同じです。

これをイミュータビリティと言うそうで、
Reactの公式サイトでも、イミュータブルな操作が重要になると記述されていました。

イミュータビリティってなんだ?

なぜ必要なのか?

いまいちピンとこなかったので、一つ一つ調べてみました。

ミュータブル・イミュータブルについて

  • ミュータブル → 値を直接変更できる(可変)
  • イミュータブル → 値を直接変更できない(不変)
// ミュータブル
	const mutable = [0, 1, 2];
	mutable.push(3);
	console.log(mutable); → [0, 1, 2, 3]

// イミュータブル
    const array = [0, 1, 2];
    const imutable = [...array, 3];
    console.log(array); → [0, 1, 2]
    console.log(imutable); → [0, 1, 2, 3]

Reactでのイミュータブルについて詳しく知るために、
JavaScriptのプリミティブ型とオブジェクト型について先にお話します。

オブジェクト型とプリミティブ型

オブジェクト型 ミュータブル(可変)

この後に説明する、プリミティブ型に該当しないもの。
オブジェクトや配列などを指す。

プリミティブ型 イミュータブル(不変)

  • 論理型(boolean): 真偽値
  • 数値型(number): 数値
  • 文字列型(string): 文字列
  • undefined型: 値が未定義であることを表す型
  • ヌル型(null): 値がないことを表す型
  • シンボル型(symbol): 一意で不変の値
  • bigint型: 数値型では扱えない大きな整数型

もともと配列やオブジェクトはオブジェクト型のミュータブルに分類されるので、
値を直接変更することができます。

しかしReactで配列やオブジェクトを管理する際は、イミュータブルとして
扱わなければならない
ようです。

なぜReactではイミュータブルが必要?

結論

  • 変更前後の値を使える
  • 変更の検出
  • 余計な再レンダリングを防ぐため

文章だけだと理解が難しかったので、
先程のコードと一緒にご説明します。

// ミュータブル
	const mutable = [0, 1, 2];
	mutable.push(3);
	①console.log(mutable); → [0, 1, 2, 3]
	②console.log(mutable); → [0, 1, 2, 3]

// イミュータブル
    const array = [0, 1, 2];
    const imutable = [...array, 3];
    ①console.log(imutable); → [0, 1, 2, 3]
	②console.log(array); → [0, 1, 2]

上のコードは[0, 1, 2]の配列に3を付け足す処理を、
ミュータブル(可変)とイミュータブル(不変)それぞれで表したものです。

①console.logでの結果はどちらも変わりませんが、注目していただきたいのが
②配列の参照先です。

ミュータブル → 元の配列([0, 1, 2])の情報はなくなる

イミュータブル → 元の配列の情報は残ったまま

まとめ

Reactでは、stateやpropsの値が更新されるたびに、Reactがコンポーネントを呼び出し、差分を計算します。
そして計算によって出された差分をもとに、ブラウザは画面を描画します。

以上を踏まえて…

ミュータブルな操作をした場合、
先程のコードでもおわかりのように、
変更前の値がなくなってしまうので、変更を辿って一つ一つ比較しなければず、
差分を計算するのにものすごくコストがかかります

ですが、イミュータブルな操作は変更前と変更後がはっきりわかるので
余計な再レンダリングがなくなり、差分の計算コストを非常に抑えることができます

以上になります!
徹底的に調べるって大事ですね…

ではまた!