リストと key

リストと key 。まずはリストの変換方法のおさらいからスタートです。

map 関数を使ったコード。

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((number) => number * 2);
console.log(doubled);  // [2, 4, 6, 8, 10]

配列を要素のリストに変換することが、上記とほぼ同様である。

複数コンポーネントをレンダーする

要素の週語を {} で囲み JSX の中に書くことができる。

配列 numbers の各要素を map<li> でくくり、変数 listItem に返す。

listItem 配列を <ul> 要素ではさみレンダーする。

      const numbers = [1, 2, 3, 4, 5];
      const listItems = numbers.map((number) => <li>{number}</li>);

      ReactDOM.render(<ul>{listItems}</ul>, document.getElementById('root'));

1から5までの数字の箇条書きリストが表示される。

## 基本的なリストコンポーネント

先程の例を numbers 配列を受け取り要素のリストを出力するコンポーネントに書き換える。

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <li>{number}</li>
  );
  return (
    <ul>{listItems}</ul>
  );
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);

コードを実行するとブラウザに期待した結果が表示されます。

Warning: Each child in a list should have a unique "key" prop.

それと同時に上記の警告も出力されます。ユニークなキーを追加する。

...
const listItem = numbers.map((number) => <li key={number.toString()}>{number}</li>);

key

key は。どの要素が要素が変更、追加もしくは削除されたのかを識別するのに役立ちます。配列内の項目に識別するために、それぞれの項目にユニークな key を与えます。

多くの場合、データ内にある IDkey として使うことになる。

const todoItems = todos.map((todo) => <li key={todo.id}>{todo.text}</li>);

データ設計時はユニークなキーフィールドに id 名を割り当ておくように留意しとこう!

レンダーされる要素に安定したID がない場合は、項目のインデックスを代用する。

const todoItems = todo.map((todo, index) =>
  <li key={index}>
    {todo.text}
  </li>
)

todoItems のようなリストは、並び順が変更されたり、追加登録されたり削除される可能性がある。 このようなリスト要素に変更が加わった場合、変更前の index とj変更後の index は変化シます。

依存するのがどれほど危険かわかりますね。

key でリンクされている Index as a key is an anti-pattern ではユニークID 生成ライブラリとして nanoid が紹介されている。 UUID しか知らなかった。勉強になります!

nanoid

A tiny, secure, URL-friendly, unique string ID generator for JavaScript.

key のあるコンポーネントの抽出

ListItem コンポーネントを抽出する際には、 key は、配列内の <ListItem /> 要素に残しておくべき。

正しい key の使用法

      function ListItem(props) {
        // こで key を指定しない
        return <li>{props.value}</li>;
      }

      function NumberList(props) {
        const numbers = props.numbers;
        const listItems = numbers.map((number) => (
          // key は配列の中で指定せよ
          <ListItem key={number.toString()} value={number} />
        ));
        return <ul>{listItems}</ul>;
      }

      const numbers = [1, 2, 3, 4, 5];
      ReactDOM.render(<NumberList numbers={numbers} />, document.getElementById('root'));

key は、それを取り囲んでいる配列側で用いる。

key の基本ルールは、map() 呼び出しの中に現れる要素に必要となる。

兄弟要素の中で一意であればよい

key は兄弟要素の中でユニークでなければならない。グローバルではユニークでなくてもよい。

次は sidebarcontent を兄弟要素として持つ Blog コンポーネントです。

      function Blog(props) {
        const sidebar = (
          <ul>
            {props.posts.map((post) => (
              <li key={post.id}>{post.title}</li>
            ))}
          </ul>
        );

        const content = props.posts.map((post) => (
          <div key={post.id}>
            <h3>{post.title}</h3>
            <p>{post.content}</p>
          </div>
        ));

        return (
          // sidebar と content は兄弟要素
          <div>
            {sidebar}
            <hr />
            {content}
          </div>
        );
      }

      const posts = [
        { id: 1, title: 'Hello World', content: 'Welcome to learning React!' },
        { id: 2, title: 'Installation', content: 'You can install React from npm.' },
      ];

      ReactDOM.render(<Blog posts={posts} />, document.getElementById('root'));

key は、コンポーネントには渡されない。 React へのヒントとして使われる。同じ値をコンポーネントでも必要となる場合は、別の名前の props として明示的に渡す。

const content = posts.map((post) =>
  <Post
    key={post.id}
    id={post.id}
    title={post.title} />
);

Post コンポーネントprops.id は読み取れるが、 props.key は読み取れない。

mapを JSX に埋め込む

map() の結果をインラインとして記述できる。

インライン化部分。

{numbers.map((number) => (
              <ListItem key={number.toString()} value={number} />
            ))}

全体のコード。

      function ListItem(props) {
        return <li>{props.value}</li>;
      }

      function NumberList(props) {
        const numbers = props.numbers;
        return (
          <ul>
            {numbers.map((number) => (
              <ListItem key={number.toString()} value={number} />
            ))}
          </ul>
        );
      }

      const numbers = [1, 2, 3, 4, 5];
      ReactDOM.render(<NumberList numbers={numbers} />, document.getElementById('root'));

map() がネストするならコンポーネントに抽出することを考慮する。

リストと key の項目はパターンが多い。頭に入り切らない。公式サイトを何度も参照することになること間違いなしです。

フォーム

9.フォーム

フォーム要素は他のDOM要素と異なる動作をする。

HTMLフォームの標準の動作は、フォームを送信した際に新しいページに移動することです。しかし、フォームの送信に応じて入力したデータにアクセスする関数があった方が便利です。

制御されたコンポーネントと呼ばれるテクニックを使うことで、実現できる。

制御されたコンポーネント

<input><textarea><select> のようなフォーム要素は、それ自身で状態を保持している。

React では、変更されうる状態は state プロパティに保持され、 setState() 関数で更新を管理する。

フォーム送信時に名前をログに残す場合を例にコンポーネントを記述する。

      class NameForm extends React.Component {
        constructor(props) {
          super(props);
          this.state = { value: '' };

          this.handleChange = this.handleChange.bind(this);
          this.handleSubmit = this.handleSubmit.bind(this);
        }

        handleChange(event) {
          // 制御された値 this.state.value を setState 関数に渡す
          // フォームに入力された値はキーストローク毎にキャプチャされる
          console.log(`[handleChange event]: ${event.target.value}`);
          this.setState({ value: event.target.value });
        }

        handleSubmit(event) {
          // 制御された値 this.state.value を setState 関数に渡す
          // submit毎にキャプチャされる
          console.log(`[handleSubmit event]: ${this.state.value}`);
          alert('A name was submitted: ' + this.state.value);
          event.preventDefault();
        }

        render() {
          return (
            <form onSubmit={this.handleSubmit}>
              <label>
                Name:
                <input type="text" value={this.state.value} onChange={this.handleChange} />
              </label>
              <input type="submit" value="Submit" />
            </form>
          );
        }
      }

      ReactDOM.render(<NameForm />, document.getElementById('root'));

<input> 要素から value={this.state.value} と属性値を渡している。これにより React の state が信頼できる情報源となる。

これでフォームの状態と setState() の状態を結合できる。別の言い方をすれば、フォームの状態を React が管理可能になったと理解できそうです。

handleChange はキーストローク毎に実行され、 state を更新する。

React の管理下になった値は、他の UI に渡したり、他のイベントハンドラからリセットできるようになる。

例では、テキストボックスに入力した値を送信ボタンをクリックする毎に alert に使いまわし通知している。

Hooks で書き換える

今記述した NameForm はクラスコンポーネントです。 Hooks で書き換える。

      function NameForm(props) {
        // 文字列は配列です
        const [name, setName] = React.useState([]);

        const handleSubmit = (e) => {
          alert('A name was submitted: ' + name);
          e.preventDefault();
        };

        return (
          <form onSubmit={handleSubmit}>
            <label>
              Name:
              <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
            </label>
            <input type="submit" value="Submit" />
          </form>
        );
      }

      ReactDOM.render(<NameForm />, document.getElementById('root'));

<input> には文字列が入力される。文字列は配列の一種なので、初期値は空の配列にする。

preventDefault();alert より先に書きたい誘惑に駆られます。だが、公式サイトの順番を守っておく。

const handleSubmit = (e) => {
  // e.preventDefault();
  alert('A name was submitted: ' + name);
  e.preventDefault();
};

textareaタグ

<textarea value={}> を使い HTML の <textarea> の代わりとする。単一行の入力フォームと似た書き方ができる。

      class EssayForm extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            value: 'Please write an essay about your favorite DOM element.',
          };

          this.handleChange = this.handleChange.bind(this);
          this.handleSubmit = this.handleSubmit.bind(this);
        }

        handleChange(event) {
          this.setState({ value: event.target.value });
        }

        handleSubmit(event) {
          alert('An essay was submitted: ' + this.state.value);
          event.preventDefault();
        }

        render() {
          return (
            <form onSubmit={this.handleSubmit}>
              <label>
                Essay:
                <textarea value={this.state.value} onChange={this.handleChange} />
              </label>
              <input type="submit" value="submit" />
            </form>
          );
        }
      }

      ReactDOM.render(<EssayForm />, document.getElementById('root'));

constructor 内で this.state.value に文字列を定義シているので、表示されるページには最初からテキストが入っている。

constructor(props) {
  super(props);
  this.state = {
    value: 'Please write an essay about your favorite DOM
element.',
  };

Hooks で書き換える

Hooks への書き換えも随分と慣れてきた」などと思っていたら違った。 EssayForm コンポーネントprops 引数がなくても動作する。いつから勘違いしていたんだ!?

      // const EssayForm = (props) => {
      const EssayForm = () => {
        const [text, setText] = React.useState([
          'Please write an essay about your favorite DOM element.',
        ]);

        const handleSubmit = (e) => {
          alert('An essay was submitted: ' + text);
          event.preventDefault();
        };

        return (
          <form onSubmit={handleSubmit}>
            <label>
              Essay:
              <textarea value={text} onChange={(e) => setText(handleChange)} />
            </label>
            <input type="submit" value="submit" />
          </form>
        );
      };

      ReactDOM.render(<EssayForm />, document.getElementById('root'));

selectタグ

HTML の selected 属性の代わりに、 親の select タグで value 属性を使う。

        constructor(props) {
          super(props);
          this.state = { value: 'coconut' };

          this.handleChange = this.handleChange.bind(this);
          this.handleSubmit = this.handleSubmit.bind(this);
        }

        handleChange(event) {
          this.setState({ value: event.target.value });
        }

        handleSubmit(event) {
          alert('Your favorite flavor is: ' + this.state.value);
          event.preventDefault();
        }

        render() {
          return (
            <form onSubmit={this.handleSubmit}>
              <label>
                Pick your favorite flavor:
                <select value={this.state.value} onChange={this.handleChange}>
                  <option value="grapefruit">Grapefruit</option>
                  <option value="lime">Lime</option>
                  <option value="coconut">Coconut</option>
                  <option value="mango">Mango</option>
                </select>
              </label>
              <input type="submit" value="Submit" />
            </form>
          );
        }
      }

      ReactDOM.render(<FlavorForm />, document.getElementById('root'));

<input type="text"><textarea><select> の3つは、 value 属性を受け取る。

複数の入力の処理

複数の input 要素を処理する場合、各入力要素に name 属性を追加し、ハンドラに event.target.name に基づいた処理を選択できる。

      class Reservation extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            isGoing: true,
            numberOfGuests: 2,
          };

          this.handleInputChange = this.handleInputChange.bind(this);
        }

        handleInputChange(event) {
          const target = event.target;
          const value = target.type === 'checkbox' ? target.checked : target.value;
          const name = target.name;

          this.setState({
            [name]: value,
          });
        }

        render() {
          return (
            <form>
              <label>
                is Going:
                <input
                  name="isGoing"
                  type="checkbox"
                  checked={this.state.isGoing}
                  onChange={this.handleInputChange}
                />
              </label>
              <br />
              <label>
                Number of guests:
                <input
                  name="numberOfGuests"
                  type="number"
                  value={this.state.numberOfGuests}
                  onChange={this.handleInputChange}
                />
              </label>
            </form>
          );
        }
      }

      ReactDOM.render(<Reservation />, document.getElementById('root'));

次の [] を用いたプロパティ名は Computed property names と呼ぶ。

// Computed property name 構文(計算されたプロパティ名)
this.setState({
  [name]: value,
});

ES5 における次のコードと同じ。

var partialState = {};
partialState[name] = value;
this.setState(partialState);

JavaScript Primer では、 プロパティの追加 で説明されている。

式の評価結果をプロパティ名に使う

MDN では 計算されたプロパティ名 で説明されている。

括弧 [] の中に式を記述でき、それが計算されてプロパティ名として使用されます。

この構文のことをすっかり忘れていた。

私のモダンJavaScript戦闘力は低い!モダンJavaScriptの速習が改めて必要です。

本格的なソリューション の中で Formik が紹介されている。 フォームはユーザ入力を受け付ける窓口だ。やはりきちんと作り込まれたライブラリがある。

日本語の情報だと CodeZineReact向けライブラリを解説 が 2020/12/07 で比較的新しい記事を読むことができる。