2020.05.04

React入門チュートリアル (3) 属性と状態


この連載記事は、これから React を学びたい JavaScript 開発者のための入門コンテンツです。対象とする React のバージョンは執筆時点で最新の v16.13 です。連載記事は以下の通り。

1️⃣ Reactとは何か
2️⃣ JSX
3️⃣ 属性と状態
4️⃣ フォームとイベントハンドリング
5️⃣ ToDoアプリを作ってみよう
6️⃣ 副作用
7️⃣ カスタムフック
8️⃣ Reactプロジェクトを始める方法

⚠ 理解を助けることを意図としているため、網羅的、リファレンス的な解説はしていません。ドキュメントを併読すると、さらに理解が深まると思います。

本章では、コンポーネントが管理するデータである props(属性)と state(状態)について学びます。React では、state は特別な意味を持ち、state が更新されると、それを管理するコンポーネントが再レンダリングされます。

コンポーネントツリーとデータ

DOM の世界に「DOM ツリー」「親要素」「子要素」などの表現があるのと同様に、React コンポーネントもツリー構造や親子関係を持つと考えられます。

たとえば以下のコンポーネント構造は…

<Parent>
  <ChildOne />
  <ChildTwo>
    <Baby />
  </ChildTwo>
</Parent>

以下の親子構造を持つと考えられます。

Parent
├─ ChildOne
└─ ChildTwo
    └─ Baby

さて、この親子関係という文脈を前提にすると、React コンポーネントが扱う2種類のデータは以下のように理解できます。

  • props:親から子へ渡される属性値
  • state:子の内部状態

親コンポーネントから渡された props は、子コンポーネント側で更新することはできません。

反対に、子コンポーネントの state = 内部状態を親が直接参照・更新することはできません。

state が更新されると、その state を持つコンポーネントが再レンダリングされます。結果として、そこから呼ばれている子コンポーネントも再レンダリングされます。

これらの性質を前提に、以下を読み進めてください。

props

外部から渡される属性値は、props と呼びます。JSX の章ですでに紹介していますね。

props には JavaScript で値として解釈されるものであればなんでも渡すことができます。文字列以外を渡したい場合は、引用符の代わりに、{} で囲う必要があります。

<Component
  string="this is string"
  number={123}
  bool={true}
  array={[9, 8, 7]}
  object={{ hello: 'world' }}
  func={a => a * 2}
  variable={something}
/>

ネイティブ属性

JSX で HTML 要素を作成する場合、必ずしも HTML の属性値と React での props 名が同じとは限りません。

<button
  id="the-button"
  className="button is-primary"
  onClick={handleClick}
>ボタン</button>

<label htmlFor="the-id">ラベル</label>

classfor は JavaScript の予約語なので、そのままは使えません。createElement に変換されることを思い出せば、当然ですね。

// ❌ "for" は予約語
React.createElement('label', { for: 'the-id' }, ['ラベル']);

また、onClick onChange などイベントを表す props は、JavaScript の通例にしたがって、キャメルケースで表現されます。

アクセシビリティ向上のために用いられる aria-* で始まる属性は、例外としてそのまま用いられます。

<input
  type="text"
  id="email"
  aria-labelledby="the-label"
  aria-required="true"
/>

イベント処理

React におけるイベント処理は、onClick などの props に関数を渡すことで実現します。

const handleClick = () => {
  // Do something...
};

const button = <button onClick={handleClick}>クリックしてね</button>;

イベント処理のための特別な構文は必要なく、props を応用して実現します。次章でもう少し詳しく見ていきます。

カスタム属性

HTML 要素には決められた属性しか渡すことはできませんが、独自要素には自由に props を定義できます。名前は JavaScript の変数名として正しいものでなければいけません。

<Component customProp={something} />

コンポーネントそのものを渡すこともできます。

<Layout
  header={<Header />}
  content={<Content />}
  footer={<Footer />}
/>

属性名のみ渡すと、その値は true と解釈されます。渡さないと undefined です。false ではないので注意しましょう。

<Panel narrow>...</Panel>

state

props は、外部からコンポーネントにデータを渡す方法でしたが、コンポーネントの内部で、データを管理したい場合があります。そのようなときには、useState 関数を用います。

useState 関数は「状態(State)」と呼ばれる、特別な働きを持つデータを作ります。

説明
第一引数 * 状態の初期値
戻り値 配列 [0]: 状態を参照する変数
[1]: 状態を更新するための関数
function CountButton() {
  const countState = React.useState(0);

  // countState[0] => 状態
  // countState[1] => 状態を更新するための関数

  const count = countState[0];
  const setCount = countState[1];

  const handleClick = () => setCount(count + 1);

  return <button onClick={handleClick}>{count}</button>;
}

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

以下が出力結果です。ボタンを押してみてください。クリックするごとに、自動的にボタンの中のテキストが更新されているはずです。

See the Pen React Tutorial Example 3.1 by Masahiro Harada (@MasahiroHarada) on CodePen.

useState を使って作った値は、コンポーネント内の「状態」を表します。状態が変化すると、コンポーネント関数(上の例で言うと CountButton)が再実行されます。その結果、レンダリングされる結果も更新されます。

これが、第一章で説明した「データと UI の同期」です。React では、開発者はどのように DOM が操作されるべきかはコーディングしません。JSX で宣言的に、どういう DOM が生成されるべきかを記述して、あとはデータの操作ロジックに専念します。

普通に考えると、CountButton 関数が再度実行されると、また count がゼロに戻って、表示は変わらないように思えます。

しかし、useState は不思議な関数です。初回の実行時は引数に渡した初期値が用いられるのですが、2回目以降の呼び出し時は、更新された値を覚えていて、初期値ではなくそちらを返します。この仕組みによって、再レンダリング時に表示結果が更新されるのです。

文法的な話をすると、useState の戻り値は、次のようにスプレッド構文を使って代入する記述が一般的です。

const [count, setCount] = React.useState(0);

次に、state による更新の影響範囲を確認できるサンプルを見ていきましょう 🤠

function Parent() {
  const [num, setNum] = React.useState(0);

  const handleClick = () => setNum(num + 1);

  return (
    <>
      <p><button onClick={handleClick}>{num}</button></p>
      <p><Child num={num} /></p>
      <p><Child num={num} /></p>
    </>
  );
}

function Child({ num }) {
  const [myNum, setMyNum] = React.useState(0);

  const handleClick = () => setMyNum(myNum + 1);

  return (
    <button className="child" onClick={handleClick}>
      {num + myNum}
    </button>
  );
}

const root = document.getElementById('root');
ReactDOM.render(<Parent />, root);
  1. Parent の state(num)が更新されると、Parent 関数が再実行されます。これにより Child も再度実行されます。
  2. ChildParent から props で num を渡されていて、JSX 内でも使用しているので、num の更新に応じて UI も更新されます。
  3. 逆に、Child の state(myNum)更新は、Parent に影響を与えません。

(以下、CodePen の Babel タブでも JavaScript コードが見られます。)

See the Pen React Tutorial Example 3.2 by Masahiro Harada (@MasahiroHarada) on CodePen.

React の便利さがこのあたりから分かり始めてもらえると思います。最後に簡単にフックについて説明して、この章を終わります。

フック

フック(Hook)とは、React の便利な機能を導入できるヘルパー関数群です。

実は、useState はフックの一つです。DOM 更新を自動化してくれる、便利な state の機能を使えるようになりましたよね。

他にもフックはいくつかあります。そのうちのいくつかは後続の章で紹介する予定です。さらに、自分でカスタムフックを作ってロジックを共通化するテクニックもあります。こちらについても後の章で説明します。

さて、いまの段階で重要なのは、フックには使用上のルールがある、という点です。

ルールは、フック関数を実行できる場所についての以下の2つです。

  • フックを呼び出すのはトップレベルのみ
  • フックを呼び出すのは React の関数内のみ

ここでは、1点目について、なぜこのようなルールが必要なのか、説明します。

そもそも「トップレベル」とはどういうことかというと、条件分岐やループの中で使ってはいけない、ということです。ただ、より本質的には、フックが実行される回数は、レンダリングごとに同一でなければならないということです。

なぜなら、React は内部で、呼び出し順によって state の値を管理しているからです。

useState の説明で、再レンダリング時は更新された値を返す、と説明しました。コンポーネントは単なる関数なので、再実行されると内部の変数は保持されず消えてしまいます。つまり、更新値を管理しているのは、関数そのものではなく、React 内部の仕組みです。

React は、どのコンポーネントで state が作られたか記録していて、作られた順に配列に入れている、というイメージで OK かと思います(実際の実装は分かりませんが)。

そういうわけで、全体の実行順が崩れるような場所でフックを呼んではいけない、というルールが存在するのです。

// ❌ NG: num の値によって実行されるかどうかが変わってしまう
if (num > 123) {
  const [flag, setFlag] = React.useState(true);
}

// ❌ NG: items の要素数によって実行回数が変わってしまう
for (let i = 0; i < items.length; i++>) {
  const [flag, setFlag] = React.useState(true);
}

🔖 参考記事:
関数型Reactコンポーネントでレンダリングと副作用Hookが実行されるタイミング

練習問題

問題 1

ランダムな猫の GIF が見られるアプリを作ってみましょう。/* ??? */ の部分を埋めて、コードを完成させてください。

// GIF 共有サイト GIPHY から持ってきた GIF ID
const gifIds = [
  '10dU7AN7xsi1I4', 'tBxyh2hbwMiqc', 'ICOgUNjpvO0PC',
  '33OrjzUFwkwEg', 'MCfhrrNN1goH6', 'rwCX06Y5XpbLG'
];

// 上記配列の要素をランダムに返す
function getGifId() {
  const max = gifIds.length;
  const index = Math.floor(Math.random() * Math.floor(max));
  return gifIds[index];
}

function Gif({ id }) {
  /* ??? 2章の練習問題1と同様 */
}

function App() {
  /* ??? GIF ID を表す state を生成する */

  const handleClick = () => {
    /* ??? ボタンが押されると GIF 画像が切り替わる */
  };

return (
    <>
      <p>
        <button onClick={handleClick}>play</button>
      </p>
      <Gif id={/* ??? */} />
    </>
  );
}

const root = document.getElementById('root');
ReactDOM.render(<App />, root);

🙌 解答例

こちらの CodePen を参照してください。

連載

  1. Reactとは何か
  2. JSX
  3. 属性と状態
  4. フォームとイベントハンドリング
  5. ToDoアプリを作ってみよう
  6. 副作用
  7. カスタムフック
  8. Reactプロジェクトを始める方法