インタラクティビティの追加

画面上の要素には、ユーザの入力に反応して更新されていくものがあります。例えば、イメージギャラリをクリックするとアクティブなイメージが切り替わります。React では、時間の経過とともに変化するデータのことを state と呼びます。任意のコンポーネントに state を追加することができ、必要に応じて更新することができます。この章では、インタラクションを処理し、state を更新し、時間の経過とともに異なる出力を表示するコンポーネントの作成方法について学びます。

イベントへの応答

React では、JSX にイベントハンドラを追加することができます。イベントハンドラは、クリック、ホバー、フォーム入力へのフォーカスなどのユーザインタラクションに応答して起動する独自の関数です。

<button> のような組み込みコンポーネントは onClick のような組み込みのブラウザイベントのみをサポートします。しかし、独自のコンポーネントを作成し、そのイベントハンドラ props に任意のアプリケーション固有の名前を付けることもできます。

export default function App() {
  return (
    <Toolbar
      onPlayMovie={() => alert('Playing!')}
      onUploadImage={() => alert('Uploading!')}
    />
  );
}

function Toolbar({ onPlayMovie, onUploadImage }) {
  return (
    <div>
      <Button onClick={onPlayMovie}>
        Play Movie
      </Button>
      <Button onClick={onUploadImage}>
        Upload Image
      </Button>
    </div>
  );
}

function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

Ready to learn this topic?

イベントへの応答を読んで、イベントハンドラの追加方法を学びましょう。

Read More

state:コンポーネントのメモリ

コンポーネントはインタラクションの結果として画面に表示されるものを変更する必要があることがよくあります。フォームに入力すると入力フィールドが更新され、イメージカルーセルで「次へ」をクリックすると表示されるイメージが変更され、「購入」をクリックするとショッピングカートに商品が入ります。コンポーネントは、現在の入力値、現在のイメージ、ショッピングカートを「記憶」する必要があるのです。React では、このようなコンポーネント固有の記憶を state と呼びます。

useState フックを使用すると、コンポーネントに state を追加することができます。フックとはコンポーネントに React の機能を使用させるための特別な関数です(state はその機能の 1 つです)。useState フックで state 変数を宣言できます。これは初期 state を受け取り、現在の state と、それを更新するための state セッタ関数のペアを返します。

const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);

以下は、イメージギャラリがクリックされたときに state を更新する方法です:

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);
  const hasNext = index < sculptureList.length - 1;

  function handleNextClick() {
    if (hasNext) {
      setIndex(index + 1);
    } else {
      setIndex(0);
    }
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleNextClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i>
        by {sculpture.artist}
      </h2>
      <h3>
        ({index + 1} of {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>
        {showMore ? 'Hide' : 'Show'} details
      </button>
      {showMore && <p>{sculpture.description}</p>}
      <img
        src={sculpture.url}
        alt={sculpture.alt}
      />
    </>
  );
}

Ready to learn this topic?

state: コンポーネントのメモリを読んで、値を記憶し、インタラクションに応答してその値を更新する方法について学びましょう。

Read More

レンダーとコミット

コンポーネントは、画面で表示される前に React によってレンダーされる必要があります。このプロセスのステップを理解することで、コードの実行方法について考え、その動作を説明することができるようになります。

コンポーネントは厨房で材料から美味しい料理を作る料理人だと想像してください。このシナリオでは React はお客様のリクエストを受け付け、注文された料理を運ぶウェイターです。注文を受けて UI 要素を「配膳」するプロセスには、次の 3 つのステップが存在します:

  1. レンダーのトリガ(お客様の注文を厨房に届ける)
  2. コンポーネントのレンダー(厨房で注文された料理を準備する)
  3. DOM へのコミット(テーブルに注文を置く)
  1. React をレストランのウェイターに見立てて、ユーザーからの注文を取り、それをコンポーネント厨房 (Component Kitchen) に届ける。
    Trigger
  2. The Card Chef gives React a fresh Card component.
    Render
  3. React delivers the Card to the user at their table.
    Commit

Illustrated by Rachel Lee Nabors

Ready to learn this topic?

レンダーとコミットを読んで、UI 更新のライフサイクルを学びましょう。

Read More

state はスナップショットである

通常の JavaScript の変数とは異なり、React の state はスナップショットのような動作をします。これを設定しても既存の state 変数は変更されず、代わりに再レンダーがトリガされます。このような動作に最初は驚くかもしれませんね。

console.log(count); // 0
setCount(count + 1); // Request a re-render with 1
console.log(count); // Still 0!

React はこのような仕組みになっているので、微妙なバグを回避することができます。ここに小さなチャットアプリがあります。まず「送信」を押して、次に受信者を「ボブ」に変更したら何が起こるか、推測してみてください。5 秒後の alert には誰の名前が表示されるでしょうか?

import { useState } from 'react';

export default function Form() {
  const [to, setTo] = useState('Alice');
  const [message, setMessage] = useState('Hello');

  function handleSubmit(e) {
    e.preventDefault();
    setTimeout(() => {
      alert(`You said ${message} to ${to}`);
    }, 5000);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        To:{' '}
        <select
          value={to}
          onChange={e => setTo(e.target.value)}>
          <option value="Alice">Alice</option>
          <option value="Bob">Bob</option>
        </select>
      </label>
      <textarea
        placeholder="Message"
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
      <button type="submit">Send</button>
    </form>
  );
}

Ready to learn this topic?

state はスナップショットであるを読んで、イベントハンドラ内で state が「固定」され、変化していないように見える理由を学びましょう。

Read More

一連の state の更新をキューに入れる

このコンポーネントにはバグがあります:“+3” をクリックしても 1 しかスコアが増えません。

import { useState } from 'react';

export default function Counter() {
  const [score, setScore] = useState(0);

  function increment() {
    setScore(score + 1);
  }

  return (
    <>
      <button onClick={() => increment()}>+1</button>
      <button onClick={() => {
        increment();
        increment();
        increment();
      }}>+3</button>
      <h1>Score: {score}</h1>
    </>
  )
}

state はスナップショットであるで、なぜこのようなことが起こってしまうのかを説明しています。state を設定すると、新しい再レンダーが要求されますが、すでに実行されているコード内の state は変更されません。そのため setScore(score + 1) を呼び出した直後は score0 であり続けます。

console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0

state を設定する際に更新用関数を渡すことでこれを修正することができます。setScore(score + 1)setScore(s => s + 1) に置き換えることで、“+3” ボタンが修正されることに注目してください。これは、複数の state の更新をキューに入れる必要がある場合に便利です。

import { useState } from 'react';

export default function Counter() {
  const [score, setScore] = useState(0);

  function increment() {
    setScore(s => s + 1);
  }

  return (
    <>
      <button onClick={() => increment()}>+1</button>
      <button onClick={() => {
        increment();
        increment();
        increment();
      }}>+3</button>
      <h1>Score: {score}</h1>
    </>
  )
}

Ready to learn this topic?

一連の state の更新をキューに入れるを読んで、次回のレンダーの前に複数の更新をキューに入れる方法を学びましょう。

Read More

state 内のオブジェクトの更新

state にはオブジェクトを含むあらゆる種類の JavaScript の値を保持することができます。しかし、React の state 内で保持するオブジェクトや配列を直接変更してはいけません。その代わり、オブジェクトや配列を更新したい場合、既存のもののコピーを作るなどして新しい値を作成し、その新しい値を使って state を変更する必要があります。

通常、変更したいオブジェクトや配列をコピーするには ... というスプレッド構文を使用します。例えば、ネストされたオブジェクトを更新する場合、次のようになります:

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    setPerson({
      ...person,
      name: e.target.value
    });
  }

  function handleTitleChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        title: e.target.value
      }
    });
  }

  function handleCityChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        city: e.target.value
      }
    });
  }

  function handleImageChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        image: e.target.value
      }
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        City:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Image:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
      <img
        src={person.artwork.image}
        alt={person.artwork.title}
      />
    </>
  );
}

コード内のオブジェクトのコピーが面倒になったら Immer のようなライブラリを使うと、コードの繰り返しを減らすことができます:

import { useImmer } from 'use-immer';

export default function Form() {
  const [person, updatePerson] = useImmer({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    updatePerson(draft => {
      draft.name = e.target.value;
    });
  }

  function handleTitleChange(e) {
    updatePerson(draft => {
      draft.artwork.title = e.target.value;
    });
  }

  function handleCityChange(e) {
    updatePerson(draft => {
      draft.artwork.city = e.target.value;
    });
  }

  function handleImageChange(e) {
    updatePerson(draft => {
      draft.artwork.image = e.target.value;
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        City:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Image:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
      <img
        src={person.artwork.image}
        alt={person.artwork.title}
      />
    </>
  );
}

Ready to learn this topic?

state 内のオブジェクトの更新を読んで、オブジェクトを正しく更新する方法を学びましょう。

Read More

state 内の配列の更新

配列もまた、state 内で保持できるミュータブル(mutable; 書き換え可能)な JavaScript オブジェクトの一種ですが、読み取り専用として扱うべきものです。オブジェクトと同様に state に保存された配列を更新したい場合、新しいものを作成し(または既存のもののコピーを作成し)、state が新しい配列を使用するようにセットする必要があります:

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [list, setList] = useState(
    initialList
  );

  function handleToggle(artworkId, nextSeen) {
    setList(list.map(artwork => {
      if (artwork.id === artworkId) {
        return { ...artwork, seen: nextSeen };
      } else {
        return artwork;
      }
    }));
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={list}
        onToggle={handleToggle} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

コード内の配列のコピーが面倒になったら Immer のようなライブラリを使うと、コードの繰り返しを減らすことができます:

import { useState } from 'react';
import { useImmer } from 'use-immer';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [list, updateList] = useImmer(initialList);

  function handleToggle(artworkId, nextSeen) {
    updateList(draft => {
      const artwork = draft.find(a =>
        a.id === artworkId
      );
      artwork.seen = nextSeen;
    });
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={list}
        onToggle={handleToggle} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

Ready to learn this topic?

state 内の配列の更新を読んで、配列を正しく更新する方法を学びましょう。

Read More

次のステップ

イベントへの応答に進んで、この章をページごとに読み進めましょう!

もしくは、すでにこれらのトピックに詳しい場合、state の管理について読んでみましょう。