かもメモ

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

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)

React 切り替えた input タグに focus させたい

React入門初心者マンです。

Reactビギナーズガイド ―コンポーネントベースのフロントエンド開発入門

Reactビギナーズガイド ―コンポーネントベースのフロントエンド開発入門

この本をやっていてエクセルのようなものを作る例があり、セルをダブルクリックしたら編集モードに切り替わるコンポーネントを作っていました。編集モードになった時に自動的にinputタグにフォーカスさせたかったのでメモ

"react": "^16.8.3",
"react-dom": "^16.8.3",

DOMにアクセスして .focus() でフォーカスさせる

状態じゃないから?結局はDOMに対して.focus()させるしかないっぽいです。

  1. input タグに ref属性を設定
  2. this.refs["ref_value"]で対象ref属性を持つDOMにアクセスできる
  3. DOMにアクセスして.focus()でフォーカスさせる
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Cell extends Component {
  componentDidUpdate() {
    if( this.props.isEdit ) {
      this.refs['text-cell'].focus();
    }
  }

  render() {
    let content = this.props.value;
    if( this.props.isEdit ) {
      content = (
        <input
          type="text"
          ref="text-cell"
          defaultValue={this.props.value}
        />
      );
    }
    return (
      <td>{content}</td>
    );
  }
}

componentDidUpdateライフサイクルメソッドでコンポーネントが更新されたタイミングで編集モードになっていればinputタグが出力されているので、ここでDOMを取得してフォーカスさせました。
 

安心と信頼のO'REILLYと思って買ったReactの本、2017年に出たものなのに既に使えなくなってるメソッドや、やり方が変わってる方法だらけでReactやゔぁい… 調べながらやってるので、その分力になってると思いたいけど、その調べた結果が正しいのかが判断しづらい点が問題。


[参考]

Reactビギナーズガイド ―コンポーネントベースのフロントエンド開発入門

Reactビギナーズガイド ―コンポーネントベースのフロントエンド開発入門

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

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

JavaScript タイプライターみたいなエフェクト作ってみた。

単純に文字列を1文字づつ追加していくだけのものですが

See the Pen Text Typewriter with Javascript Promise by KIKIKI (@chaika-design) on CodePen.

※ 表示エフェクトはForkしたものですが、表示ロジックは丸っと作り直ました

PromiseはsetTimeoutを繰り返してもresolveを待つっぽい

エフェクトの終了を取りたかったのでとりあえずPromiseで実装してみたのですが、興味深いことに内部でsetTimeoutを繰り返していても最終的なresolveが実行されるのを待つようです。

実装していた部分

const typewriter = ($elm) => {    
  return new Promise((resolve, reject) => {
    const typeSpeed = 100;
    if($elm.length) {
      const text  = $elm.innerText;
      const chars = text.length;
      let index = 0;
      $elm.innerHTML = '';

      const write = () => {
        $elm.innerText += text[index];
        if(index < chars) {
          index += 1;
          setTimeout(() => write(), typeSpeed);
        } else {
          $elm.innerHTML = text;
          resolve('TYPEWRTER complete');
        }
      };
      write();
    } else {
      reject('TYPEWRTER no content');
    }
  });
};

文字列の長さ分setTimeoutを繰り返して最終的にresolveしています。呼び出し元はこのresolveを待ってthenが実行されました。
どうやらPromiseを返す関数は内部にresolverejectが有ることをチェックしてから実行されているっぽいですね。
関数内にresolverejectがあるけど、無限ループや動的な条件でresolveに辿り着けないような関数だとどうなるのでしょう…雰囲気的に待ち続けてしまいそうな気もしますが、タイムアウト的なところでundefinedresolveされるのでしょうか? (試す気がない


[参考]

Macのキーボードとほぼ同じなのでiPadで使うのにちょうどよい感じでした。