React公式サイトのドキュメントが2023年3月16日に改訂されました(「Introducing react.dev」参照)。本稿は、基本解説の「Updating Arrays in State」をかいつまんでまとめた記事です。ただし、コードにはTypeScriptを加えました。反面、初心者向けのJavaScriptの基礎的な説明は省いています。
なお、本シリーズ解説の他の記事については「React + TypeScript: React公式ドキュメントの基本解説『Learn React』を学ぶ」をご参照ください。
配列はJavaScriptではミュータブルです。けれど、Reactで状態に収める場合は、イミュータブルとして扱ってください。配列もオブジェクトです。したがって、状態に入れた配列の更新には、まず新たな配列(またはもと配列の複製)をつくります。そのうえで、編集を加えて状態に設定するのです。
配列をイミュータブルに更新する
JavaScriptでは、配列はオブジェクトのひとつです。Reactでは、オブジェクトと同じく、配列も読み取り専用として扱ってください。つまり、配列インデックスで指定した要素への代入はできません。また、もと配列を書き替える、Array.prototype.push()
やArray.prototype.pop()
などのミュータブルなメソッドも使わないということです。
状態に収めた配列の更新は、新たな配列に編集を加えて状態設定関数により行います。配列操作に用いるのは、もと配列は触らないArray.prototype.filter()
やArray.prototype.map()
といったイミュータブルなメソッドです。
つぎの表には、よく行われる配列操作を示しました。右の列が推奨されるイミュータブルなやり方です。
操作 | 非推奨(ミュータブル) | 推奨(イミュータブル) |
---|---|---|
追加 |
push() 、unshift()
|
concat() 、スプレッド構文...
|
削除 |
pop() 、shift() 、splice()
|
filter() 、slice()
|
置き換え |
splice() 、インデックスへの代入 |
map() 、reduce()
|
ソート |
reverse() 、sort()
|
配列を複製して操作 |
配列に要素を加える
Array.prototype.push()
は、もとの配列をミュータブルに書き替えます。状態の更新に用いることは避けてください。かわりに、もと配列の複製を新たにつくったうえで、加える要素はそのあとに収めればよいのです。やり方はいくつかあります。簡単なのは配列のスプレッド構文...
を使うことでしょう(サンプル001)。
import { useState } from 'react';
type Artist = {
id: number;
name: string;
};
let nextId = 0;
export default function List() {
const [name, setName] = useState('');
const [artists, setArtists] = useState<Artist[]>([]);
const handleClick = () => {
setArtists([...artists, { id: nextId++, name }]);
setName('');
};
return (
<>
<h1>Inspiring sculptors:</h1>
<input
value={name}
onChange={({ target: { value } }) => setName(value)}
/>
<button onClick={handleClick}>Add</button>
<ul>
{artists.map(({ id, name }) => (
<li key={id}>{name}</li>
))}
</ul>
</>
);
}
サンプル001■React + TypeScript: Updating Arrays in State 01
配列インデックスはオブジェクトのプロパティと異なり、(キーの)重複は起こりません。したがって、加える要素ははじめに置いて、もと配列をあとから収めれば、先頭に加えることもできます。
配列から要素を除く
配列から要素を除くときに使うと簡単なのがArray.prototype.filter()
メソッドです。元配列から条件に合わない要素を外した新たな配列が返されます。条件には、削除する要素のオブジェクト(artist
)から取り出した一意のプロパティ(id
)を指定しました(サンプル002)。
import { useState } from 'react';
type Artist = {
id: number;
name: string;
};
let initialArtists: Artist[] = [
{ id: 0, name: 'Marta Colvin Andrade' },
{ id: 1, name: 'Lamidi Olonade Fakeye' },
{ id: 2, name: 'Louise Nevelson' }
];
export default function App() {
const [artists, setArtists] = useState(initialArtists);
const handleClick = (artist: Artist) => {
setArtists(artists.filter(({ id }) => id !== artist.id));
};
return (
<>
<h1>Inspiring sculptors:</h1>
<ul>
{artists.map((artist) => (
<li key={artist.id}>
{artist.name}{' '}
<button
onClick={() => {
handleClick(artist);
}}
>
Delete
</button>
</li>
))}
</ul>
</>
);
}
サンプル002■React + TypeScript: Updating Arrays in State 02
配列の要素を変換する
もと配列の要素を、それぞれの値に応じて書き替えるために使うのがArray.prototype.map()
メソッドです。引数のコールバック関数には、それぞれの要素にどのような手を加えるのか定めます。戻り値は、書き替えられた要素が収められた新たな配列です。
つぎのコード例では、配列要素のオブジェクトのプロパティ値により、処理をふたつに条件分けしました。それぞれの場合に、要素オブジェクトの異なったプロパティ値を変更しています。これらの値はJSX要素のスタイル(style
)に反映していますので、画面上で要素の位置やかたち、カラーなどがアニメーションで変化するでしょう(サンプル003)。
type Shape = {
id: number;
type: string;
x: number;
y: number;
color: string;
};
const initialShapes: Shape[] = [
{ id: 0, type: 'circle', x: 50, y: 100, color: 'purple' },
{ id: 1, type: 'square', x: 150, y: 100, color: 'purple' },
{ id: 2, type: 'circle', x: 250, y: 100, color: 'purple' }
];
export default function ShapeEditor() {
const [shapes, setShapes] = useState(initialShapes);
const handleClickMove = () => {
const nextShapes = shapes.map((shape) =>
shape.type === 'square'
? {
...shape,
type: 'circle',
color: 'deeppink'
}
: {
...shape,
y: shape.y + 50
}
);
setShapes(nextShapes);
};
}
サンプル003■React + TypeScript: Updating Arrays in State 03
配列に要素を挿入する
配列中の指定インデックスに要素を加えたいときがあります。この場合の扱い方は、スプレッド構文...
とArray.prototype.slice()
メソッドの組み合わせです。スプレッド構文で展開した要素をArray.prototype.slice()
で挿入インデックスの前後ふたつに切り分けます。そのうえで、挿入要素をふたつの間に挟めばよいのです(サンプル004)。なお、Array.prototype.slice()
もイミュータブルなメソッドで、新たな配列を返します。
export type Artist = {
id: number;
name: string;
};
let nextId = 3;
const initialArtists: Artist[] = [
{ id: 0, name: 'Marta Colvin Andrade' },
{ id: 1, name: 'Lamidi Olonade Fakeye' },
{ id: 2, name: 'Louise Nevelson' }
];
export default function App() {
const [name, setName] = useState('');
const [artists, setArtists] = useState(initialArtists);
const handleClick = (index: number) => {
const nextArtists = [
...artists.slice(0, index),
{ id: nextId++, name },
...artists.slice(index)
];
setArtists(nextArtists);
setName('');
};
}
サンプル004■React + TypeScript: Updating Arrays in State 04
配列要素の順序を変える
もと配列を書き替えるArray
のミュータブルなメソッドは、使う前に必ずもと配列は複製してください。たとえば、配列要素の順序を変えるArray.prototype.sort()
やArray.prototype.reverse()
です。つぎのコード例は、Array.prototype.reverse()
を、スプレッド構文でつくったもと配列の複製(nextArtists
)に対して呼び出しています(サンプル005)。
export default function App() {
const [artists, setArtists] = useState(initialArtists);
const handleClickReverse = () => {
const nextArtists = [...artists];
nextArtists.reverse();
setArtists(nextArtists);
};
}
サンプル005■React + TypeScript: Updating Arrays in State 05
配列要素のオフジェクトを更新する
配列の中の複数の要素を更新したいことがあるでしょう。配列が状態に収められているとき、要素は多くの場合データのオブジェクトです。要素のオブジェクトは実際には配列中にはなく、参照をもっているだけだということにご注意ください。同じオブジェクトを別のコードから参照しているかもしれないからです。
入れ子になったオブジェクトを書き替えるには、その階層までたどって複製しなければなりません。つぎのコードは、配列要素のオブジェクト(artist
)はスプレッド構文で展開したうえで、プロパティ(checked
)を変更する例です(サンプル006)。
export type Artist = {
id: number;
name: string;
checked: boolean;
};
const initialArtists: Artist[] = [
{ id: 0, name: 'Marta Colvin Andrade', checked: false },
{ id: 1, name: 'Lamidi Olonade Fakeye', checked: false },
{ id: 2, name: 'Louise Nevelson', checked: true }
];
export default function App() {
const [artists, setArtists] = useState(initialArtists);
const handleToggle = (id: number, checked: boolean) => {
setArtists(
artists.map((artist) => {
if (artist.id === id) {
return { ...artist, checked };
} else {
return artist;
}
})
);
};
}
サンプル006■React + TypeScript: Updating Arrays in State 06
前掲コード例のハンドラ関数を、以下のように書いてはいけません。たしかに、スプレッド構文でもと配列(artists
)の第1階層は複製しています。けれど、更新する要素(artist
)がオブジェクトのままです。そのため、プロパティ(checked
)の書き替えは、ミュータブルな変更になります。
具体的には、初期値の配列(initialArtists
)でも、参照する同じ要素のオブジェクトが変わってしまうのです(console.log()
で確かめられます)。たとえば、初期値に戻す処理が求められたときには問題になるでしょう。
const handleToggle = (id: number, checked: boolean) => {
const nextArtists = [...artists];
const artist = nextArtists.find((artist) => artist.id === id);
if (artist) {
artist.checked = checked;
}
/* setArtists(
artists.map((artist) => {
if (artist.id === id) {
return { ...artist, checked };
} else {
return artist;
}
})
); */
setArtists(nextArtists);
console.log(initialArtists); // 初期値の配列も変更される
};
配列の状態をImmerでイミュータブルに更新する
配列の第一階層からひとつ下って、オブジェクトをスプレッド構文で展開するくらいならさほど問題はありません。けれど、入れ子が深くなると手間がかかるだけでなく、バグも生じやすくなります。まず考えていただきたいのは、入れ子はできるだけ浅くすることです。それでも、状態を構造化したい場合はあるでしょう。
ライブラリImmerを使えば、オブジェクトのデータ構造はイミュータブルに保ったまま、簡単に変更が加えられます(「React + TypeScript: Immerで状態をイミュータブルに保つ」参照)。前掲サンプル006のコードをImmerで書き替えてみましょう。
ここで用いるのはuseImmer
フックです(「フックuseImmerを使う」参照)。イミュータブルな状態からつくったミュータブルな状態に手を加えます。構文は、useState
フックとほぼ変わりません。戻り値の配列の第1要素が現在の状態、第2要素は設定関数です。フックの引数には状態の初期値を与えてください。
状態設定関数に渡すのは、コールバックです。コールバック関数は、ミュータブルに変換されたオブジェクトを引数(draft
)として受け取ります。したがって、このオブジェクトはどのように書き替えても構いません。もとのイミュータブルな状態(オブジェクト)は、別にそのまま保たれるのです。動作は、以下のサンプル007でお確かめください。
import { useImmer } from 'use-immer';
export type Artist = {
id: number;
name: string;
checked: boolean;
};
const initialArtists: Artist[] = [
{ id: 0, name: 'Marta Colvin Andrade', checked: false },
{ id: 1, name: 'Lamidi Olonade Fakeye', checked: false },
{ id: 2, name: 'Louise Nevelson', checked: true }
];
export default function App() {
const [artists, updateArtists] = useImmer(initialArtists);
const handleToggle = (id: number, checked: boolean) => {
updateArtists((draft) => {
const artist = draft.find((artist) => artist.id === id);
if (!artist) return;
artist.checked = checked;
});
};
}
サンプル007■React + TypeScript: Updating Arrays in State 07
さて、お気づきでしょうか。このハンドラ関数(handleToggle
)の記述は、前述の書いてはいけないミュータブルなコードと同じ組み立てです。コールバック関数が受け取る引数(draft
)はミュータブルに扱ってよいので、もと配列が変更されるメソッドやプロパティの書き替えも気にせずに使えます。もちろん、もとのイミュータブルな状態(オブジェクト)は、別にそのまま保たれるのです。
まとめ
この記事では、つぎのような項目についてご説明しました。
- 状態に収めた配列は直接変更してはいけません。
- 配列はイミュータブルに扱い、新たにつくった配列に手を加えて状態に定めてください。
- 配列のスプレッド構文
...
を使うと、もと配列が複製できます。 - 処理したあとの配列を新たに返すメソッドとしてよく用いられるのが、つぎのふたつです。
-
Array.prototype.filter()
: 条件に合った要素の抽出。 -
Array.prototype.map()
: すべての配列要素を変換。
-
- ライブラリImmerは、オブジェクトのデータ構造をイミュータブルに保って変更します。