かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React ソートしたときの key にハマる

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は変わらないものを指定する

ソートなどで並び順が変わるようリストの場合、keyindexなどにしていると変更内容が 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')
);

React Unsortable List Element

👇 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>);
    });
    // ...
  }
}

React Sortable List

なんとなくですが、リスト内の子コンポーネント(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がデフォルトとして使用される
  • keypropsとして渡されない
  • 並び順が変わるようなリストの場合indexkeyにするとパフォーマンスが悪く、stateに問題がでる場合もあるので良くない

まとめ

Reactは頑張って英語の本家ドキュメントを読もう! (変化が早いのでそれが確実な近道...)

失敗を恐れるな! アイカツ格言 第17話


[参考]

React入門 React・Reduxの導入からサーバサイドレンダリングによるUXの向上まで (NEXT ONE)

React入門 React・Reduxの導入からサーバサイドレンダリングによるUXの向上まで (NEXT ONE)