インタラクティブ機能の追加

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

イベントへの対応

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

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

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>
  );
}

このトピックを学習する準備はできましたか?

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

詳細を読む

状態:コンポーネントのメモリ

コンポーネントは、インタラクションの結果として画面上の内容を変更する必要があることがよくあります。フォームに入力すると入力フィールドが更新され、「次へ」をクリックすると表示される画像が変更され、「購入」をクリックすると商品がショッピングカートに追加されます。コンポーネントは、現在の入力値、現在の画像、ショッピングカートなど、物事を「記憶」する必要があります。React では、この種のコンポーネント固有のメモリは状態と呼ばれます。

useStateフックを使用して、コンポーネントに状態を追加できます。フックは、コンポーネントが React の機能(状態はその 1 つ)を使用できるようにする特別な関数です。useStateフックを使用すると、状態変数を宣言できます。初期状態を受け取り、2 つの値のペア(現在の状態と、それを更新できる状態設定関数)を返します。

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

クリック時に画像ギャラリーが状態を使用および更新する方法を次に示します。

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

このトピックを学習する準備はできましたか?

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

詳細を読む

レンダリングとコミット

コンポーネントが画面に表示される前に、React によってレンダリングされる必要があります。このプロセスのステップを理解することで、コードの実行方法と動作を説明するのに役立ちます。

コンポーネントがキッチンで美味しい料理を材料から組み立てている料理人だと想像してみてください。このシナリオでは、React は客からの注文を受け取り、注文を持って来るウェイターです。この UI の要求と提供のプロセスには 3 つのステップがあります。

  1. トリガーによるレンダリング(客の注文をキッチンに届ける)
  2. レンダリングコンポーネント(キッチンで注文を準備する)
  3. コミット DOM へ(注文をテーブルに置く)
  1. React as a server in a restaurant, fetching orders from the users and delivering them to the Component Kitchen.
    トリガー
  2. The Card Chef gives React a fresh Card component.
    レンダリング
  3. React delivers the Card to the user at their table.
    コミット

このトピックを学習する準備はできましたか?

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

詳細を読む

スナップショットとしての状態

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

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

この動作により、微妙なバグを回避できます。小さなチャットアプリを例に挙げてみましょう。「送信」ボタンを先に押し、その後受信者をBobに変更した場合に何が起こるか考えてみてください。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>
  );
}

このトピックを学習する準備はできましたか?

スナップショットとしての状態を読んで、イベントハンドラ内で状態が「固定」され、不変に見える理由を学びましょう。

詳細を読む

一連の状態更新のキューイング

このコンポーネントはバグがあります。「+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>
    </>
  )
}

スナップショットとしての状態では、これがなぜ起こるのかを説明しています。状態を設定すると新しい再レンダリングが要求されますが、既に実行中のコード内の状態は変更されません。scoreは、setScore(score + 1)を呼び出した後も0のままです。

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

状態を設定する際に、更新関数を渡すことでこれを修正できます。setScore(score + 1)setScore(s => s + 1)に置き換えると「+3」ボタンが修正されることに注目してください。これにより、複数の状態更新をキューイングできます。

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>
    </>
  )
}

このトピックを学習する準備はできましたか?

一連の状態更新のキューイングを読んで、状態更新のシーケンスをキューイングする方法を学びましょう。

詳細を読む

状態内のオブジェクトの更新

状態には、オブジェクトを含むあらゆる種類のJavaScript値を格納できます。しかし、Reactの状態に直接保持しているオブジェクトや配列を変更しないでください。オブジェクトや配列を更新する場合は、新しいオブジェクト(または既存オブジェクトのコピー)を作成し、そのコピーを使用して状態を更新する必要があります。

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

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のようなライブラリを使用して、反復的なコードを削減できます。

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

このトピックを学習する準備はできましたか?

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

詳細を読む

状態内の配列の更新

配列は、状態に格納できる別の種類の変更可能なJavaScriptオブジェクトであり、読み取り専用として扱う必要があります。オブジェクトと同様に、状態に格納されている配列を更新する場合は、新しい配列(または既存配列のコピー)を作成し、新しい配列を使用して状態を設定する必要があります。

import { useState } from 'react';

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のようなライブラリを使用して、反復的なコードを削減できます。

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

このトピックを学習する準備はできましたか?

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

詳細を読む

次は?

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

または、これらのトピックに既に精通している場合は、状態の管理について読んでみてはいかがでしょうか?