はじめに
こんにちは、iimon新卒エンジニアのみやこしです、最近業務でreactを使うことが多くなり、reactをお勉強中です!勉強し始めた頃にmapメソッドを使う際に引っかかったので、今回もこのお話についてさせて頂きます。
map() とは Array インスタンスのメソッドで、与えられた関数を配列のすべての要素に対して呼び出し、その結果からなる新しい配列を生成します。
Array.prototype.map() - JavaScript | MDN
map()を使う際に起きた問題
import { useState } from "react"; const AddInfo = () => { const info = [ { id: "1", name: "iimonくん", }, { id: "2", name: "カメじい", }, ]; const [addInfo ,setInfo] = useState(info); const addNewInfo = () => { const newInfo = { id: "3", name: "いいねちゃん", }; setInfo([newInfo,...addInfo]); // 既存の配列に新しいオブジェクトを追加 }; return ( <div> <div> <button onClick={addNewInfo}>追加</button> <ul> {addInfo.map((item) => ( <li >{item.name} メモ: <input type="text" /></li> ))} </ul> </div> </div> ) }; export default AddInfo;
useStateの勉強で簡単なメンバーの情報を追加するリストを実装する際に、上記のコードみたいに、infoの中のメンバーのリストをmapメソッドで呼び出し、要素liタグに入れて画面にレンダーする。その時に以下のエラーが起きた。 これはリスト内のものはユニークなID値を持つkeyプロパティを設定が必要だと怒られています。Reactが要素のリスト内で変更、追加、削除されたアイテムを識別するためだからです、そこでmapメソッドの引数のindexを使ってliタグにそれぞれの識別値を持たせた。
import { useState } from "react"; const AddInfo = () => { const info = [ { id: "1", name: "iimonくん", }, { id: "2", name: "カメじい", }, ]; const [addInfo ,setInfo] = useState(info); const addNewInfo = () => { const newInfo = { id: "3", name: "いいねちゃん", }; setInfo([newInfo,...addInfo]); // 既存の配列に新しいオブジェクトを追加 }; return ( <div> <div> <button onClick={addNewInfo}>追加</button> <ul> {addInfo.map((item,index) => ( <li key={index}>{item.name} メモ: <input type="text" /></li> ))} </ul> </div> </div> ) }; export default AddInfo;
エラーが無事消えました!が、ここから問題が起きました! メンバーを追加するボタンを押した時に、それぞれのメンバーの横にあるinputの値がずれたのです!
追加前: 追加後: 本来のメンバーに新しいメンバー追加することによってinputの値が一個ずつ上にズレたことがわかります。色々調べた結果React の再レンダリングに辿り着いて、再レンダリングについて改めました。
React の再レンダリングの仕組み
再レンダリングについて話す前にまずReactの基本的な構造についておさらいします。 reactは仮想DOMというDOMを仮に構築することでJavaScriptの変更をこの仮想DOMに適用して、最適化された差分だけをリアルのDOMに反映します。初めてページを表示する際、仮想DOMはリアルのDOMをミラーリングすることになります。図で表すと以下のようにイメージです。 データに変化が発生した時、reactは新しいデータに基づいて、新しい仮想DOMが作られます、仮想DOM同士で比較をし、その差分だけリアルDOMで反映されます。 本題の再レンダリングは以下の三つの場合に行われます:
今回はstateの変更された時に再レンダリングしたことがわかります。また、Reactは差分検出を行うとき要素の型までチェックしてくれます。上の例だとliタグの要素が一つ増えて、新しい仮想DOMツリーがそれを検出し、その差分だけが更新されることになります。
map()メソッドでkeyをindexにするのを避けたい
再レンダリングについて改めて学習し、先ほどのkeyをindexした時inputの値がズレる原因がやっと見つけられました!結論各データに固有の識別値を使うときはid,電話番号など使用することがお勧めされます。keyをindexにする時とkeyをidにする時の違いを比較してみました!
イメージのようにindexをkeyにすると新しいメンバーが入ることによってkeyが変わり、仮想DOMはliのkeyが0のものを更新前のデータと比較します、更新前のデータと違うとを検出し新しいデータが更新され、リアルDOMに反映します。liのタグの中にあるinput要素をチェックする際、変わりがなかったため、そのままリアルDOMで使われます。しかし、inputにvalueが入っていることは予想外だったのです。そうすると元々keyが0のinputのvalueが新しいメンバー追加されたことによって、ズレが発生します。indexをkeyにすると3回更新されていることがわかります。
import { useState } from "react"; const AddInfo = () => { const info = [ { id: "1", name: "iimonくん", }, { id: "2", name: "カメじい", }, ]; const [addInfo ,setInfo] = useState(info); const addNewInfo = () => { const newInfo = { id: "3", name: "いいねちゃん", }; setInfo([newInfo,...addInfo]); // 既存の配列に新しいオブジェクトを追加 }; return ( <div> <div> <button onClick={addNewInfo}>追加</button> <ul> {addInfo.map((item) => ( <li key={item.id}>{item.name} メモ: <input type="text" /></li> ))} </ul> </div> </div> ) }; export default AddInfo;
新しいメンバーを追加した結果:
idをkeyにした時は同じく仮想DOMが更新前のデータと比較してみました。イメージでわかるようにliのkeyが3のデータだけ更新されます。
以上の比較で、indexをkeyにすると理想のデータとの違くなることがあり、また元々データの更新が1回で済むものが3回更新することによってパフォーマンスの低下に繋がることがわかりました。
まとめ
最後まで読んでくださり、ありがとうございます! React の再レンダリングの仕組みと差分検出について改めて知ることができました、今回の学習を踏まえ今後パフォーマンス向上に繋がるコードを書けるように心がけます。 この記事を読んで興味を持って下さった方がいらっしゃれば、カジュアルにお話させていただきたいです。是非ご応募をお願いいたします!
参考
Reactの仮想DOMが理解できてなかった!? #再レンダリング - Qiita
ものすごく丁寧に解説 初心者のためのReact 再レンダリングの仕組みと最適化 #JavaScript - Qiita