【React向け】配列の反復処理を理解して見通し良く使う
Gaji-Labo フロントエンドエンジニアの茶木です。
スタートアップのプロダクト開発支援をしている私たちはさまざまな技術スタックに触れるのですが、一番多いのは React です。React 開発には、慣れれば有用なものの、初めてではつまづく、いわゆる “初見殺し” がいくつかあります。今日の初見殺しは配列(Array) の反復処理です。
配列の反復処理は React 固有ではありませんが、React でデータを操作するにはほぼ必須です。 例として、一覧ページは、詳細ページのアイテムのリストと言えますが、リストはアイテムの配列のデータとして扱われるためです。
Array の反復処理とは
配列の要素に対して操作を行う処理です。
今日触れるのは、次のメソッドです。
- map
- filter (every, some)
- reduce
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array
特に for
文に親しんだ人が、はじめて配列のメソッドを使うとき思考形態ごと変える必要があり、盛大な初見殺しとなると思います。(体験談)
map
items.map((item) => {
return `${item.firstName} ${item.familyName}`
});
配列の各要素 ( item
)を参照して、新しい要素を作って、新しい配列の要素として返す、というものです。この例では、氏名を要素とした新しい配列を作っています。
覚えておくとよいのは、配列の型は変わるが、配列の要素数は保たれたままということです。
ワンライナー
items.map((item) => `${item.firstName} ${item.familyName}`);
1行で書けると可読性が高まります。
多くの場合、短いコードの方が可読性が高いので、{ return (); }
を省略する形で書けないか?という点は常に自問するとよいでしょう。 return
の前に行が存在すると短く書けないので工夫のしどころです。
filter
items.filter((item) => !item.canceled );
filter
も頻出の処理です。
配列から条件に当てはまるもの(引数が真と評価されるもの)だけを抜き出します。
覚えておくとよいのは、map
と逆で、配列の型は変わらず、配列の要素数は減るという点です。
items
.filter((item) => !item.canceled )
.map((item) => (<li key={item.id}>{item.name}</li>));
filter
に関わらずですが配列を返すメソッドには、小さいテクニックがあり、 items.filter(xxx).map(xxx)
と .
で繋ぐメソッドチェーンが使えます。
最後のメソッドチェーンで、map
を指定し、DOM要素に変換するのがよくあるパターンです。
every, some
if(items.every((item) => item.canceled )) return <ZeroResults />
if(!(items.some((item) => !item.canceled ))) return <ZeroResults />
items.filter(xxx).length === 0
を使うようなケース、たとえば一覧ページで表示するものがあるかどうか調べたいときに使えます。 every
は引数の判定がすべて true
, some
は引数の判定のうち一つ以上が true
のときそれぞれ true
を返します。
どちらを使っても、実装可能ですが、判定式が単純になるほうを使うべきです。
また、filter
はすべての要素にアクセスしますが、every
は false
、 some
は true
をそれぞれ見つけたとき処理を終えて結果を返すので、全要素にアクセスする filter
より計算コストが低くなります。
reduce
items.reduce((acc, cur) => {
return [...acc, cur];
}, []);
おそらく最難関の処理が reduce
です。
上記は、完全な説明用で、reduce
前後で配列を変わらないので使い道はありません。
まず、第2引数の空の配列に注目します。以下のようにループは進行します。
- 1度目のループでは、
acc
はこの空の配列になる - 1度目のループの返り値が 2度目のループの acc になる
- 2度目のループの返り値が 3度目のループの acc になる
- …
- こうして
acc
に蓄積( accumlate )されていき、最後のループの返り値が処理結果になる
cur
は 各ループの要素なので、return の [...acc, cur]
とすることで、ループごとに各要素を配列の最後に追加することになります。これが基本形になります。
items.reduce((acc, cur) => {
return acc.some((a) => a.id === cur.id) ?
acc : [...acc, cur];
}, []);
次に、実践的なもので重複する id
を持つ要素の除外処理です。
一致する id
の要素があれば、 cur
を加えずに return
しています。
注目すべきは、cur
を含めずにループが進む回があるため、reduce
は配列の長さを変えうる操作になるという点です。
items.reduce((acc, cur) => {
const index = acc.findIndex((a) => a.id === cur.id);
if(index === -1) return [...acc, { ...cur, amount: 1}];
const res = [...acc];
res[index].amount += 1;
return res;
}, []);
アドバンスの例です。
重複する id
を持つ要素の除外処理に加え、重複した id
の数 amount
を要素に加えています。
覚えておくとよいのは、配列の型も要素数も変更可能なメソッドであることです。
補足: 第2引数はなんでも取れる
items.reduce((acc, cur) => {
if (!cur.id in acc) return {...acc, [cur.id]: 1 }
const res = {...acc};
res[cur.id] += 1;
return res;
}, { example: 1});
ここまでの例では、第2引数に配列を指定していましたが、任意の型の値を指定できます。また必ずしも空である必要もありません。この例では初期値の入ったオブジェクトに、追加のキーとして重複数を記録しています。
reduce
は自身の要素を合成しながらループする性質上、理解が難しいものの、柔軟な操作ができ、使いこなすと強力な武器になります。
まとめ
他のメソッド
flat
,flatMap
: 二重配列を合成してネストを解消する操作をします。生の二重配列を扱う機会が少ないので、使用頻度は高くないです。これらを使うためにメソッドチェーンで事前に処理して二重配列にすることもあります。sort
,reverse
: 配列の順序を操作します。リストの並べ替えや昇順、降順の操作に使用が想定されますが、外部データ由来であれば、リクエスト時点でソート済みなので使用頻度は低そうです。forEach
: 使いません。一見難しくても他の反復メソッドで可換です。特に、外にカウント用のlet
変数を置くようであれば、forEach
ではなくreduce
が使える公算が大です。例外はレンダリングに影響しないサイドエフェクト的な、通知APIなどの関数を呼びたい場合です。
TypeScript
TypeScript では型が強く意識されます。操作後に配列の要素の型が変わるメソッド map
, reduce
では、返り値が想定の型かどうかに注目すると良いと思います。特に reduce
の第2引数は、制限がなく見失いがちです。
メソッドごとに、配列長と配列要素の型を変更するかまとめておきます。
メソッド | 配列長 | 配列の要素の型 |
map, flat, flatMap | 変わらない | 変わる |
reduce | 変わる | 変わる |
filter | 変わる | 変わらない |
sort , reverse | 変わらない | 変わらない |
おわりに
配列操作は、エンジニアの思考コストを使う部分で、不具合を作り込みやすい部分でもあります。
リーダブルなコードに保つことで、チームで共有しやすく、変更に強いプロダクトを作っていけると考えています。
この記事に書いたような React やフロントエンドのさまざまな技術で良いプロダクトを作る仕事をしてみたい方がいたら、いますぐ転職する予定でなくてもカジュアルにお話ができたらうれしいです。技術を使って人やチームを支えることが好きな方、ぜひお話しましょう!
Gaji-Laboでは、React経験が豊富なフロントエンドエンジニアを募集しています
弊社ではReactの知見で事業作りに貢献したいフロントエンドエンジニアを募集しています。大きな制作会社や事業会社とはひと味もふた味も違うGaji-Laboを味わいに来ませんか?
もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください。お仕事お問い合わせや採用への応募、共に大歓迎です!
求人応募してみる!