1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ReactのStateを直接変更してはいけない理由と正しい更新方法

1
Last updated at Posted at 2026-06-30

はじめに

React の State では、オブジェクトや配列を扱うときに 直接変更しない ことが重要です。

オブジェクトや配列を直接変更せず、新しいオブジェクトや新しい配列を作って更新することを、イミュータブルに扱う と言います。

たとえば、次のような State があるとします。

const [user, setUser] = useState({
  name: 'Taro',
  age: 20,
});

この user はオブジェクトです。

このとき、次のように直接値を書き換えるのは避けるべきです。

user.age = 21; // NG
setUser(user);

一見、age21 に変わっているように見えます。

しかし、React の State では、このような直接変更は推奨されません。

なぜ直接変更してはいけないのか

React は State が変化したかどうかを判断するときに、主に 参照が変わったかどうか を見ます。

JavaScript のオブジェクトや配列は、値そのものではなく参照で比較されます。

const a = { name: 'Taro' };
const b = a;

b.name = 'Jiro';

console.log(a === b); // true

この例では、b.name を変更していますが、ab は同じオブジェクトを参照しています。

そのため、a === btrue になります。

React の State でも同じように、元のオブジェクトを直接変更して setUser(user) としても、React から見ると「同じオブジェクトが渡された」と判断される可能性があります。

その結果、再レンダリングが期待通りに行われない場合があります。

オブジェクトを更新する方法

オブジェクトを更新するときは、スプレッド構文を使って新しいオブジェクトを作ります。

setUser({
  ...user,
  age: 21,
});

これは、元の user を直接変更しているのではありません。

{
  ...user,
  age: 21,
}

によって、元の user の内容をコピーした新しいオブジェクトを作っています。

そのうえで、age だけを 21 に上書きしています。

たとえば、元の user が次のような値だったとします。

const user = {
  name: 'Taro',
  age: 20,
};

このとき、

setUser({
  ...user,
  age: 21,
});

によって、次のような新しいオブジェクトが作られます。

{
  name: 'Taro',
  age: 21,
}

重要なのは、元の user を書き換えるのではなく、変更後の新しいオブジェクトを作って setUser に渡している点です。

プロパティを追加する場合

オブジェクトに新しいプロパティを追加する場合も、直接追加するのではなく、新しいオブジェクトを作ります。

setUser({
  ...user,
  email: 'taro@example.com',
});

これは、元の user の内容をコピーしつつ、email プロパティを追加しています。

プロパティを削除する場合

プロパティを削除したい場合は、分割代入と rest 構文を使う方法があります。

const { age, ...newUser } = user;
setUser(newUser);

これは、user から age を取り出し、それ以外のプロパティを newUser にまとめる書き方です。

たとえば、元の user が次のような値だったとします。

const user = {
  name: 'Taro',
  age: 20,
  email: 'taro@example.com',
};

このとき、

const { age, ...newUser } = user;

を書くと、newUser は次のようになります。

{
  name: 'Taro',
  email: 'taro@example.com',
}

つまり、age を除いた新しいオブジェクトを作ることができます。

delete user.age のように書くと、元のオブジェクトを直接変更してしまうため、React の State 更新では避けるべきです。

配列を更新する方法

配列もオブジェクトと同じように、直接変更しないようにします。

たとえば、次のような State があるとします。

const [items, setItems] = useState(['apple', 'banana']);

この配列に要素を追加したい場合、次のように push を使うのは避けます。

items.push('orange'); // NG
setItems(items);

push は元の配列そのものを変更します。

React では、元の配列を直接変更するのではなく、新しい配列を作って更新します。

setItems([...items, 'orange']);

これは、元の items の内容をコピーし、その末尾に 'orange' を追加した新しい配列を作っています。

配列から要素を削除する場合

配列から要素を削除する場合は、filter を使うことが多いです。

setItems(items.filter(item => item !== 'banana'));

filter は、条件に合う要素だけを残した新しい配列を返します。

この例では、'banana' 以外の要素だけを残した新しい配列を作っています。

配列の中の特定の要素を変更する場合

配列の中にオブジェクトが入っている場合を考えます。

const [todos, setTodos] = useState([
  { id: 1, text: '勉強する', done: false },
  { id: 2, text: '寝る', done: false },
]);

このうち、id1 の Todo だけを完了済みにしたい場合は、map を使います。

setTodos(
  todos.map(todo =>
    todo.id === 1
      ? { ...todo, done: true }
      : todo
  )
);

このコードでは、todos.map(...) によって、元の配列をもとに新しい配列を作っています。

map は、配列の要素を1つずつ取り出し、その戻り値を集めて新しい配列を作るメソッドです。

今回の場合、各 todo に対して次の処理をしています。

todo.id === 1
  ? { ...todo, done: true }
  : todo

これは、次のような意味です。

  • todo.id === 1 なら、done: true にした新しい Todo オブジェクトを返す
  • それ以外なら、元の Todo をそのまま返す

つまり、id1 の Todo だけを新しいオブジェクトに置き換え、それ以外の Todo はそのままにした新しい配列を作っています。

ここでも重要なのは、元の Todo オブジェクトを直接変更していないことです。

{ ...todo, done: true }

によって、元の todo をコピーし、done だけを true にした新しいオブジェクトを作っています。

map は新しい配列を作る

map は、必ず新しい配列を返します。

たとえば、次のコードを考えます。

const numbers = [1, 2, 3];

const result = numbers.map(num => {
  return num * 2;
});

console.log(result);

結果は次のようになります。

[2, 4, 6]

このように、map は各要素を変換し、その結果を集めた新しい配列を作ります。

一方で、次のように console.log だけを書いた場合は注意が必要です。

const result = numbers.map(num => {
  console.log(num);
});

この場合、console.log(num) によって 1, 2, 3 は表示されます。

しかし、map の中の関数が何も return していないため、作られる新しい配列は次のようになります。

[undefined, undefined, undefined]

つまり、map は中身を確認するためにも使えますが、本来の目的は「各要素を変換して新しい配列を作ること」です。

中身を確認したいだけなら、forEach を使う方が自然です。

numbers.forEach(num => {
  console.log(num);
});

React の State 更新で map を使うのは、配列の中の特定の要素を変更した新しい配列を作りたいからです。

前の State を使って更新する場合

State の更新が、前の State に基づく場合は、関数形式で書くこともできます。

setUser(prev => ({
  ...prev,
  age: prev.age + 1,
}));

配列の場合も同じです。

setItems(prev => [...prev, 'orange']);

この書き方では、最新の State を prev として受け取り、それをもとに新しい State を作ります。

連続して State 更新が起こる可能性がある場合は、この関数形式の方が安全です。

Todo の例も、次のように書けます。

setTodos(prev =>
  prev.map(todo =>
    todo.id === 1
      ? { ...todo, done: true }
      : todo
  )
);

まとめ

React の State では、オブジェクトや配列を直接変更せず、新しい値を作って更新することが重要です。

直接変更してしまうと、React が State の変化を正しく検知できず、再レンダリングが期待通りに行われない可能性があります。

そのため、オブジェクトはスプレッド構文で新しいオブジェクトを作り、配列はスプレッド構文・filtermap などを使って新しい配列を作ります。

React の State 更新は、「元の値を書き換える」のではなく、「変更後の新しい State を作る」と考えると理解しやすいです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?