React入門初心者マンとして引き続きエクセル的なモノを作るチュートリアルをやっています。
今回テーブルをソートしようとしてハマったのでメモ。
react: ^16.8.3 react-dom: ^16.8.3
リストのような配列から作られるエレメントには key を指定する
V-DOMはDOMと同じツリー構造になっていて、構造を比較して違いがあるとその箇所を実際のDOMに反映させる仕組み。
リスト構造は変更があっても、リスト内のどこに変更があったのか判断できない。
keyが設定されていると、keyを頼りにどこが変更されたのかをReactが判断できるので、更新の反映が最小限になるって事っぽい。
配列を.mapして作成するようなエレメントはkeyを付けないとwarningになる
e. g.
import React, {Component} from 'react'; import ReactDOM from 'react-dom'; const data = [ ['0315', '星宮いちご'], ['0131', '霧矢あおい'], ['0803', '紫吹蘭'], ]; class List extends Component { render() { const list = this.props.data.map((row, i) => { return(<li>ID[ {row[0]} ]: {row[1]}</li>); }); return (<ul>{list}</ul>); } } ReactDOM.render( <List data={data} />, document.getElementById('app') );
keyを指定するようにワーニングが出る
Warning: Each child in a list should have a unique "key" prop.
map()で作られるリスト要素にkey属性を付ければOK
class List extends Component { render() { const list = this.props.data.map((row, i) => { return(<li key={i}>ID[ {row[0]} ]: {row[1]}</li>); }); return (<ul>{list}</ul>); } }
コンポーネントな時はmap()で内にある側だけにkeyを設定すれば良い
function ListItem(props) { const row = props.data; // こちらに key を設定する必要はない return (<li>ID[ {row[0]} ]: {row[1]}</li>); } class List extends Component { render() { const list = this.props.data.map((row, i) => { return(<ListItem key={i} data={row} />); }); return (<ul>{list}</ul>); } }
map()で作っている所にkeyが無ければワーニング
function ListItem(props) { const row = props.data; return (<li key={props.idx}>ID[ {row[0]} ]: {row[1]}</li>); } class List extends Component { render() { const list = this.props.data.map((row, i) => { // ここに key がないとダメ return(<ListItem idx={i} data={row} />); }); return (<ul>{list}</ul>); } }
👉 Warning: Each child in a list should have a unique "key" prop.
keyは変わらないものを指定する
ソートなどで並び順が変わるようリストの場合、keyをindexなどにしていると変更内容が render されない場合がある
sample
See the Pen React List Element Sort TEST by KIKIKI (@chaika-design) on CodePen.
🙅♂️keyの指定がなかったり、indexがしていされているとソートされないパターン
リスト内のコンポーネントがstateを持つ場合、リストのkeyの指定がなかったり、keyがindexになっているとソートでデータの並び順が変わっても正しくrenderされない
const data = [ ['0315', '星宮いちご'], ['0131', '霧矢あおい'], ['0803', '紫吹蘭'], ]; // state を持つコンポーネント class ListContentWithState extends React.Component { constructor(props) { super(props); this.state = { data: this.props.data, } } render() { const data = this.state.data; return (<span>ID[ {data[0]} ]: {data[1]}</span>); } } class List extends React.Component { constructor(props) { super(props); this.state = { data: this.props.initData, descending: false, } } _onSort() { const data = this.state.data.slice(); const descending = !this.state.descending; console.log('> sort', (descending? 'DESC':'ASC')); data.sort((a, b) => { if( descending ) { return a[0] < b[0]? 1:-1; } else { return a[0] > b[0]? 1:-1; } }); this.setState({ data: data, descending: descending, }); } _onReset() { console.log('> reset'); this.setState({ data: this.props.initData, descending: false, }); } render() { console.log('> render', this.state.data); const list = this.state.data.map((row, i) => { // keyが無かったり、indexのようにソート時に変わってしまうものだと、 // stateが変更されてもrenderされない return(<li key={i}><ListContentWithState data={row} /></li>); }); return ( <div> <button onClick={(e)=>this._onSort()}>SORT</button> <button onClick={(e)=>this._onReset()}>RESET</button> <ul>{list}</ul> </div> ); } } ReactDOM.render( <List initData={data} />, document.getElementById('app') );

👇 keyの指定をソートしても変わらないものにすればOK
class List extends React.Component { // ... render() { const list = this.state.data.map((row, i) => { // ソートしても変わらないkeyを指定すればOK const listID = row[0]; return(<li key={listID}><ListContentWithState data={row} /></li>); }); // ... } }

なんとなくですが、リスト内の子コンポーネント(ListContentWithState)がstateを持っている場合、子コンポーネント作成時にまずkeyが渡されて、そのkeyに該当するデータからstateを返しているから、最初の並びの順番で子コンポーネントが返され親コンポーネント(List)のstate.dataの順番は変更されているのに、renderはその順番で出力されないい。という感じなのではないかという印象です。
なので、子コンポーネントがstateを持たなければ、親コンポーネントのリストのkeyは指定がなくても、ソート時に変更されてしまうindexでもソートされた内容の通りにrenderされます。が、恐らくkeyの指定がありReactが変更箇所だけをうまく変更してくれるより処理は重いのではないかと思います。
👇
🙆♂️keyの指定がなかったり、indexで指定されていてもソートされるパターン
1. リストエレメントの内容が直接書かれている場合
class List extends React.Component { constructor(props) { super(props); this.state = { data: this.props.initData, descending: false, } } _onSort() { const data = this.state.data.slice(); const descending = !this.state.descending; console.log('> sort', (descending? 'DESC':'ASC')); data.sort((a, b) => { if( descending ) { return a[0] < b[0]? 1:-1; } else { return a[0] > b[0]? 1:-1; } }); this.setState({ data: data, descending: descending, }); } _onReset() { console.log('> reset'); this.setState({ data: this.props.initData, descending: false, }); } render() { const list = this.state.data.map((row, i) => { // リストエレメントの key が index / 無し でもソートは反映され描画される return(<li key={i}>ID[ {row[0]} ]: {row[1]}</li>); }); return ( <div> <button onClick={(e)=>this._onSort()}>SORT</button> <button onClick={(e)=>this._onReset()}>RESET</button> <ul>{list}</ul> </div> ); } } ReactDOM.render( <List initData={data} />, document.getElementById('app') );
2. リストエレメント内にコンポーネントを持っているがstateを使っていない場合
リスト内にあるコンポーネントがstateを持っていないならkeyは無くてもindexを指定していてもソートがrenderされる
// state を持たないコンポーネント function ListContent(props) { const data = props.data; return (<span>ID[ {data[0]} ]: {data[1]}</span>); } class List extends React.Component { constructor(props) { super(props); this.state = { data: this.props.initData, descending: false, } } _onSort() { const data = this.state.data.slice(); const descending = !this.state.descending; console.log('> sort', (descending? 'DESC':'ASC')); data.sort((a, b) => { if( descending ) { return a[0] < b[0]? 1:-1; } else { return a[0] > b[0]? 1:-1; } }); this.setState({ data: data, descending: descending, }); } _onReset() { console.log('> reset'); this.setState({ data: this.props.initData, descending: false, }); } render() { const list = this.state.data.map((row, i) => { // state を持たないコンポーネントなら key が index でもOK return(<li key={i}><ListContent data={row} /></li>); }); return ( <div> <button onClick={(e)=>this._onSort()}>SORT</button> <button onClick={(e)=>this._onReset()}>RESET</button> <ul>{list}</ul> </div> ); } } ReactDOM.render( <List initData={data} />, document.getElementById('app') );
key
map()で作られるようなリストエレメントにはkeyを設定したほうがパフォーマンスが良いkeyはリスト内でユニークなものにする
keyは同じ反復するエレメント内だけでユニークであればよく、globalとしてユニークである必要はないkeyは React自身のツリー構造のチェックに使われる- 明示的に
keyを指定しない場合はindexがデフォルトとして使用される keyはpropsとして渡されない- 並び順が変わるようなリストの場合
indexをkeyにするとパフォーマンスが悪く、stateに問題がでる場合もあるので良くない
まとめ
Reactは頑張って英語の本家ドキュメントを読もう! (変化が早いのでそれが確実な近道...)

[参考]

React入門 React・Reduxの導入からサーバサイドレンダリングによるUXの向上まで (NEXT ONE)
- 作者: 穴井宏幸,石井直矢,柴田和祈,三宮肇
- 出版社/メーカー: 翔泳社
- 発売日: 2018/02/19
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る