0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【ステート編②】useStateについて

0
Posted at

今回はReactの基礎として大変重要な概念であるstateについて学習していきます。

入力した値を画面に表示したい

前回学んだonChangeを使って、入力欄の値をそのまま画面に表示する仕組みを作ってみましょう。

まず思いつくのは「値が変わったタイミングで変数に格納し、その変数を画面に出す」という方法だと思います。実際に書いてみます。

//Example.tsx
import "./Example.css"
const Example = ()=> {
  let displayValue = "";

  const changeHandler = (e: React.ChangeEvent<HTMLInputElement>)=> {
    displayValue = e.target.value;
  }

  return (
    <div className="component">
      <input type="text" onChange={changeHandler} />
      <h3>{displayValue}</h3>
    </div>
  );
}

export default Example;

一見うまくいきそうですが、入力欄に文字を打っても画面には何も表示されません。

なぜ画面が変わらないのか

理由は2つあります。

1つ目は、コールバック関数の中しか再実行されないからです。

onChangeで実行されるのは、あくまでchangeHandlerの中だけです。returnに続くTSXの部分は再実行されません。画面の表示を作り直すには、Exampleという関数自体を再実行する必要があります。

代入自体は行われているのに、画面を作る部分が動いていない、という状態です。

2つ目は、再実行されると変数が初期化されてしまうからです。

仮にExampleが再実行されたとしても、let displayValue = "";という変数宣言が再び呼ばれるので、値は空に戻ってしまいます。いくら代入しても、画面上に表示されるのは空のままです。

つまり画面を変えるには、次の2つが必要だということが分かります。

  1. Reactにコンポーネントを再実行してもらい、新しいReact要素を作ってもらう
  2. 変更した値をどこかに保持しておく

この1つ目を再レンダリング、2つ目の保持しておく場所を**state(状態)**と呼びます。どちらも重要なキーワードなので覚えておいてください。

これらを解決するのがuseStateという関数です。

useStateを使ってみる

まずはReactのライブラリからuseStateをインポートします。

//Example.tsx
import { useState } from "react"
import "./Example.css"

const Example = ()=> {
  const valueArray = useState("");

  console.log(valueArray);

  const changeHandler = (e: React.ChangeEvent<HTMLInputElement>)=> {
  }

  return (
    <div className="component">
      <input type="text" onChange={changeHandler} />
    </div>
  );
}

export default Example;

useStateの引数には初期値を渡します。試しにuseState(0)としてコンソールを見ると、戻り値は配列になっていることが分かります。

  • 配列の0番目 … 現在の値(初期値に渡した0が入っている)
  • 配列の1番目 … 値を更新するための関数

この2つを使って書くとこうなります。

//Example.tsx
import { useState } from "react"
import "./Example.css"

const Example = ()=> {
  const valueArray = useState("");
  const setFunction = valueArray[1];

  const changeHandler = (e: React.ChangeEvent<HTMLInputElement>)=> {
    setFunction(e.target.value);
  }

  return (
    <div className="component">
      <input type="text" onChange={changeHandler} />
      <h3>{valueArray[0]}</h3>
    </div>
  );
}

export default Example;

入力欄に111と打つと、画面にも111と表示されるようになりました。

分割代入で書く

添字で指定するのは読みにくいので、実際には分割代入で受け取ります。

//Example.tsx
import { useState } from "react"
import "./Example.css"

const Example = ()=> {
  const [value, setValue] = useState("");

  const changeHandler = (e: React.ChangeEvent<HTMLInputElement>)=> {
    setValue(e.target.value);
  }

  return (
    <div className="component">
      <input type="text" onChange={changeHandler} />
      <h3>{value}</h3>
    </div>
  );
}

export default Example;

取得用の値と更新用の関数を、それぞれvaluesetValueという名前で受け取っています。setXxxという名前にするのが慣習です。

本当に再実行されているのか確認する

コンポーネントの先頭にconsole.logを置いて確かめてみましょう。

const Example = ()=> {
  const [value, setValue] = useState("");

  console.log("再レンダリングされました");

  ...
}

useStateを使わなかったときは、画面の初期表示のとき1回しか出力されませんでした。useStateを使うと、入力するたびに再レンダリングされましたと出力されます。

つまりsetValueを呼んだタイミングで、関数コンポーネント自体が再実行されているということです。これは大変重要な概念なので覚えておいてください。

useStateが行っていること

useStateが何をしているのか、順を追って整理します。

  1. 実行されたタイミングでReact内部と接続し、「このコンポーネントには状態の管理が必要ですよ」と知らせる
  2. 初期値が現在の値として保持され、ペアになる更新用関数も準備される
  3. その結果として[値, 更新用関数]という配列が返される
  4. 更新用関数に値を渡すと、React内部の値が入れ替わり、あわせてコンポーネントを再実行してくださいという指示が送られる

更新用関数が「値の入れ替え」と「再実行の依頼」の2つの役割を持っている、というのがポイントです。

このReact内部で保持管理される値のことをstateと呼びます。状態とも呼ばれ、その管理を状態管理と言います。

useStateReactフックと呼ばれる関数の1つです。フックには「何かに接続する」という意味があり、React内部で値を保持する仕組みと接続する、というイメージです。

stateはコンポーネントごとに保持管理されます。レンダリングをまたいで値を保持しておきたいときは、stateに保存する、と覚えておいてください。

useStateの呼び出しルール

useStateはコンポーネントのトップレベルでしか呼べません。

// これはエラー
const [count, setCount] = useState(0);

const Example = ()=> {
  ...
}
// これもエラー
const Example = ()=> {
  if (flag) {
    const [count, setCount] = useState(0);
  }
  ...
}

こう書くと、次のエラーが出ます。

React Hook "useState" is called conditionally.
Hooks must be called in the exact same order in every component render.

if文だけでなく、for文やwhile文の中も同様にエラーになります。

理由は、Reactの内部でstateが呼ばれた順番で管理されているからです。1番目に宣言したstate、2番目に宣言したstate、という形で保持されているので、条件によって呼ばれたり呼ばれなかったりすると、順番が崩れて別の値を取ってきてしまいます。

そのため、useStateは必ずコンポーネントのトップレベルで、毎回同じ順番で呼ぶというルールになっています。

更新用関数はすぐには反映されない

もう1つ重要な性質があります。カウンターを作って確かめてみましょう。

//Example.tsx
import { useState } from "react"
import "./Example.css"

const Example = ()=> {
  const [count, setCount] = useState(0);

  const countUp = ()=> {
    setCount(count + 1);
    console.log(count);
  }

  return (
    <div className="component">
      <p>カウント{count}</p>
      <button onClick={countUp}></button>
    </div>
  );
}

export default Example;

初期値は0です。ボタンを押すとsetCount(count + 1)1が設定されるので、直後のconsole.log(count)1になりそうなものですが、実際には0が出力されます。

更新用関数は「React内部の値を変えてください」という指示を出すだけで、その場では反映されません。次のレンダリングのときの値にする、という予約をしているだけです。これを専門的には非同期で処理されると表現します。

そのため、こう書いても結果は1にしかなりません。

const countUp = ()=> {
  setCount(count + 1);
  setCount(count + 1);
}

この時点のcountはどちらも0なので、「stateを1にしてください」という指示を2回出しているだけになるからです。

前の値を使いたいときは関数を渡す

現在の値をもとに更新したい場合は、更新用関数に関数を渡します。

const countUp = ()=> {
  setCount((prevState)=> prevState + 1);
  setCount((prevState)=> prevState + 1);
}

こう書くと、前回設定された値が引数に渡ってきて、その戻り値が次のstateの値になります。今度はボタンを1回押すと2まで増えます。

stateを変更するときは現在の値を見て処理する場合が多いので、この書き方はぜひ覚えておいてください。

確認問題

以下のExample.tsxは、ボタンを押しても画面のカウントが増えません。

//Example.tsx
import "./Example.css"
const Example = ()=> {
  let count = 0;

  const countUp = ()=> {
    count = count + 1;
    console.log(count);
  }

  return (
    <div className="component">
      <p>カウント{count}</p>
      <button onClick={countUp}></button>
    </div>
  );
}

export default Example;

問題

  1. コンソールには数字が増えて表示されるのに、画面の表示が変わらない理由を2つ挙げてください
  2. useStateを使って正しく動くように書き換えてください
  3. ボタンを1回押したときに2ずつ増えるようにするには、どう書けばよいか答えてください

(下に解答を記載しています)

解答

1. 理由

  • onClickで実行されるのはcountUpの中だけで、returnに続くTSXは再実行されないから
  • 仮にExampleが再実行されても、let count = 0;で初期化されてしまうから

2. useStateで書き換える

//Example.tsx
import { useState } from "react"
import "./Example.css"

const Example = ()=> {
  const [count, setCount] = useState(0);

  const countUp = ()=> {
    setCount(count + 1);
  }

  return (
    <div className="component">
      <p>カウント{count}</p>
      <button onClick={countUp}></button>
    </div>
  );
}

export default Example;

3. 1回で2ずつ増やす

const countUp = ()=> {
  setCount((prevState)=> prevState + 1);
  setCount((prevState)=> prevState + 1);
}

setCount(count + 1)を2回書いても1にしかなりません。前回の値を受け取る関数を渡すことで、2回分が正しく反映されます。

チェックリスト

  • 変数に代入しても画面が変わらない理由を2つ説明できる
  • 再レンダリングとstateという2つのキーワードの意味が分かる
  • useStateの戻り値が[値, 更新用関数]の配列だと理解している
  • 分割代入で受け取り、更新用関数はsetXxxという名前にしている
  • 更新用関数を呼ぶとコンポーネントが再実行されると知っている
  • useStateはコンポーネントのトップレベルでのみ呼んでいる
  • if文やfor文の中で呼ぶとエラーになる理由を説明できる
  • 更新用関数の結果はすぐには反映されない(非同期)と理解している
  • 前の値を使いたいときは更新用関数に関数を渡している

ステート編②はここまで。
次回はstateの扱い方について学習していきます!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?