はじめに
JavaScriptを勉強していて、あまり見かけない書き方をみて戸惑ってしまう方も多いかと思うので、そんな方が学習の際にチラッと見に来ていただけるといいなと思い書きました。
Reactを学ぶ上で配列操作が重要な理由
Reactは配列操作をイミュータブルで行うことが推奨されており、元の配列の内容を書き換えてしまうpush()
メソッドなどは使用できません。
イミュータブルとミュータブル
Reactでは配列を直接書き換えるのではなく、配列操作をした新しい配列をつくる必要があります。
イミュータブルの対義語はミュータブルです。
ミュータブル: 元のデータが直接変更される
イミュータブル: 元のデータは変更されない
Reactの差分検知方法
イミュータブルでなければならない理由
それは、Reactがコンポーネントの変化をオブジェクトの 同一性(差分) チェックで検知しているためです。(他にもありますが...)
Reactは浅い比較(shallow comparison)で状態を検知します。オブジェクトや配列は参照型のため、内容が変わっても参照が同じなら「変化なし」となって再レンダリングしない場合があります。
TODOアプリでタスクを追加したのに、なにも変化しない。なんてことが起こります。
ミュータブルな操作をしてしまうとコピー元の情報も変更されてしまうため、
変更前と変更後の差分をReactが検知できなくなってしまいます。
そのため、配列をもとの配列を直接書き換えるのではなく、コピーを作成したり、新しい配列を作成することにより差分を検知しレンダリングを制御する必要があります。
配列を書き換えるメソッド(破壊的メソッド)
それではまず配列を直接書き換えるメソッドはどんなものがあるかをみていきます。
書き換えるメソッドは破壊的メソッドと呼ばれます。その名の通り、元の配列を破壊します。
実際に試すとわかりやすいので、ぜひ開発者ツールのコンソールで試してみてください。
要素の追加・削除
const arr = [1,2,3]
// 以下、それぞれ上記のarrをもとに説明。
// 末尾に追加
arr.push(4) // [1,2,3,4]
// 末尾を削除
arr.pop() // [1,2]
// 先頭に追加
arr.unshift(0) // [0,1,2,3]
// 先頭から削除
arr.shift() // [2,3]
並び替え
const numbers = [3,1,5,4,2]
const words = ['qiita','react','javascript']
// 並び替え
numbers.sort(); // 文字列として比較 [1, 2, 3, 4, 5]
numbers.sort((a,b)=> a - b) // 数値として昇順 [1, 2, 3, 4, 5]
numbers.sort((a,b)=> b - a) // 数値として降順順 [5, 4, 3, 2, 1]
words.sort(); // アルファベット順 ['javascript', 'qiita', 'react']
words.sort((a,b)=> a.length - b.length); // 文字数順 ['qiita', 'react', 'javascript']
// 反転
numbers.reverse(); // [2, 4, 5, 1, 3]
words.reverse(); // ['javascript', 'react', 'qiita']
指定位置で置き換え
const arr = [1,2,3]
// 以下、それぞれ上記のarrをもとに説明。
arr.splice(1, 1) // index番号1から1つ削除 [1, 3]
arr.splice(1, 0, 2); // index番号1に2を挿入[1, 2, 2, 3]
arr.splice(1,2, "qiita", "react") // index番号1から2個削除して文字列を挿入
配列をコピーする方法とその違い
配列を書き換えないメソッドの前に、配列をコピーする方法をまとめます。
Reactを勉強していると、いままで使うことのなかったスプレッド構文というものを知ることになります。
スプレッド構文は[...original]
という感じで.(ドット)を3つ配列の変数の前に書くことで浅いコピーのオブジェクトを作成することができます。
他に、slice()
メソッドやArray.from()
メソッドでコピーを作ることができます。
const originalArray = [1, 2, 3, 4, 5];
const spreadArray = [...originalArray]; // [1, 2, 3, 4, 5]
const sliceArray = originalArray.slice(); // [1, 2, 3, 4, 5]
const fromArray = Array.from(originalArray); // [1, 2, 3, 4, 5]
3つとも配列を完全コピーすることができるんですが、それぞれ違いがあります。
項目 | スプレッド構文 | slice() | Array.from() |
---|---|---|---|
パフォーマンス | ⭐⭐⭐ 速い | ⭐⭐⭐ 速い | ⭐⭐ やや遅い |
可読性 | ⭐⭐⭐ 直感的 | ⭐⭐ 慣れが必要 | ⭐⭐ 明示的 |
柔軟性 | ⭐⭐ 基本的 | ⭐⭐⭐ 部分コピー可 | ⭐⭐⭐ 変換も可能 |
ブラウザ対応 | ES2015+ | ES3+ | ES2015+ |
配列以外対応 | ✅ | ❌ | ✅ |
配列のコピーをする方法はArray.from()
など色々ありますが、配列のコピーとなるとスプレッド構文かslice()
になるかと思います。
Reactのコードをみると、ほぼスプレッド構文です。
破壊的メソッド(sort()
、reverse()
等)は、スプレッド構文でコピーを作成してから使用することで、元の配列を変更せずに新しい配列を作成、並び替えなどの処理をして、Reactの差分検知により再レンダリングされます。
Reactでよく使われる配列メソッド(非破壊的メソッド)
ここからはReactのコードでよく見るであろうメソッドを紹介します。
slice() 部分コピーができる
const original = [1, 2, 3, 4, 5];
// 完全コピー
const copy1 = original.slice(); // [1, 2, 3, 4, 5]
// 部分コピー
// 範囲指定
const copy2 = original.slice(1, 4); // [2, 3, 4]
const copy3 = original.slice(-2, -1); // [4]
// 指定位置から末尾まで
const copy4 = original.slice(2); // [3, 4, 5]
// 末尾から指定分コピー
const copy5 = original.slice(-2); // [4, 5]
map() 変換しながらコピー
const original = [1, 2, 3, 4, 5]
// そのままコピー
const copy1 = original.map(x => x); // [1, 2, 3, 4, 5]
// 変換しながらコピー
const copy2 = original.map(x => x * 2); // [2, 4, 6, 8, 10]
map()
はJSX内で要素を一つずつ取り出して表示する際によく使われます。
const fruitList = () => {
const fruits = ['Apple', 'Banana', 'Mango', 'Grape'];
return (
<ul>
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li> // 各フルーツ名を取り出して表示
))}
</ul>
);
};
/* 一つずつレンダリングされる
<ul>
<li>Apple</li>
<li>Banana</li>
<li>Mango</li>
<li>Grape</li>
</ul>
*/
filter() 条件で絞り込んだ配列を作る
const original = [1, 2, 3, 4, 5]
// そのままコピー
const copy1 = original.filter(x => x); // [1, 2, 3, 4, 5]
// 絞り込みしてコピー
const copy2 = original.filter(x => x % 2 === 0); // [2, 4]
スプレッド構文 シンプルでわかりやすく、汎用的
スプレッド構文は構文ですが、元の配列を書き換えない配列に対する処理方法としてまとめます。
const original = [1, 2, 3, 4, 5];
// 完全コピー
const copy1 = [...original]; // [1, 2, 3, 4, 5];
// コピーと同時に要素を追加する
const copy2 = [...original, 6]; // [1, 2, 3, 4, 5, 6];
const copy3 = [...original, 6, 7]; // [1, 2, 3, 4, 5, 6, 7];
// 先頭に要素追加
const copy4 = [0, ...original]; // [0, 1, 2, 3, 4, 5];
// 複数の配列を結合する
const arr1 = [1, 2];
const arr2 = [3, 4];
const arr3 = [5, 6];
const combiArr1 = [...arr1, ...arr2]; // [1, 2, 3, 4]
const combiArr2 = [...arr1, ...arr2, ...arr3]; // [1, 2, 3, 4, 5, 6]
また、スプレッド構文は配列以外もコピーできます。
オブジェクトのコピー
const user = { name: 'John', age: 20 };
// オブジェクトのコピー
const newUser = {...user}; // { name: 'John', age: 20 }
// 新しいプロパティの追加
const extendUser = {
...user, // userの中身のコピーして新しいオブジェクトをつくる
email: 'john@qiita.jp', // あとから追加したものが追加される
isActive: true
}; // { name: 'John', age: 20, email: 'john@qiita.jp', isActive: true }
// プロパティの更新
const updatedUser = {
...user, // userの中身のコピーして新しいオブジェクトをつくる
name: 'Maria', // あとから追加したものが上書きされる
age: '45'
}; // {name: 'Maria', age: '45'}
プロパティの追加・更新の際に...user
を後にすると元のuserで上書きされてしまうので、更新・追加したい場合は先に記述します。
文字列を分割して配列にする
const string = "hello";
const characters = [...string]; // ['h', 'e', 'l', 'l', 'o']
Set(重複しないコレクション)
Setはコレクションの一種で重複した値を許さず、同じ値を追加しても1つだけが保持されます。配列のメソッド(map()
など)は使用できませんが、スプレッド構文を挟むことで配列に変換でき、使用することができます。
// 重複した数字をSetで除き、配列に変換する
const setNumbers = new Set([1, 2, 2, 3, 4, 4]);
const copySetNumbers = [...setNumbers]; // [1, 2, 3, 4]
// 重複した文字列をSetで除き、配列に変換する
const setTags = new Set(['Qiita', 'GitHub', 'React', 'Qiita', 'React', 'VSCode', 'VSCode']);
// {'Qiita', 'GitHub', 'React', 'VSCode'}
const copySetTags = [...setTags]; // ['Qiita', 'GitHub', 'React', 'VSCode']
// Setのコレクションを変換してmap()で各要素を2倍にする
const numberSet = new Set([1, 1, 2, 2, 3, 4, 5]); // {1, 2, 3, 4, 5}
const doubleNumbers = [...numberSet].map(num => num * 2); // [2, 4, 6, 8, 10]
最後に
実際のReactのコードではより複雑な使い方をすると思いますが、その際に少しでも役立てたら嬉しいです。
参考ページ