ReactのuseStateを徹底的に理解する

Reactを学び始めてからSPA(シングルページアプリケーション)の良さが理解できたところでState Management(状態管理)を学んでいる方、useStateが理解できない方、このコンセプトを理解することは、コンポーネントのレンダーにもかかわる重要な部分になります。

Reduxなどのライブラリを使う前にも理解しておきたいです。

State Management

State Managementはデータを管理するフロントエンド側のデータ倉庫のようなものです。これらはReact Hookの機能のうちのひとつです。React Hook(フック)はコンポーネントがマウント(読み込み)されたときにフック(実装)される機能と考えてください。

では実際にReactアプリを作成してコードを書きながらStateManagementを理解していきましょう。Reactのプロジェクトの作成の仕方はこちらの記事を読んでください。

まずは、メインのコンポーネントになるsrc/App.jsxの中身を削除して下記のように記述しましょう。

useState()はreactのデフォルトで使えるStateManagementのメソッドになります。これをインポートして

App()のコンポーネント内に定義しましょう。const [count, setCount] でuseStateメソッドの最初の値(データになる部分)をcountという名称にしました。

2番目の値useState[1]にあたる部分は、とsetCountという変数名にしました。慣習として2つ目に来るsetterはsetで始まる名称を付けることが通常になります。setterというのはstate(状態)が変わることを指示をする関数になります。

import { useState } from "react";


export default function App() {
  const [count, setCount] = useState(10)
    
  return (
    <div className="App">{count}</div>
  );
}

これで<div>内の{count}の部分がデフォルトの10として表示されます。

ではこの10に1の数字を足す関数を作ってみましょう。

まずは悪い例を紹介します。

※これは悪い例です。

import { useState } from "react";

export default function App() {
  const [count, setCount] = useState(10);

  function addOne() {
    count++;
  }
  return (
    <div className="App">
      <button onClick={addOne}>Count = {count}</button>
    </div>
  );
}

このボタンをクリックすると、addOne関数が発火されますが何も起きません。。。コンソールを見てみると下記の様なエラーが出ました。

App.jsx:7 Uncaught TypeError: Assignment to constant variable.

constは再代入できない変数なのでエラーが出ました。

ではこのconst [count, setCount] = useState(10);letに変えるとどうなるでしょうか?

。。。。。。

。。。。

何も起きません。

しかもエラーも出ません。

なぜでしょうか。

にletを使って変数の値が変えられたとしてもReact側では、state(状態)が変化されたことが察知されていないからです。

ReactのState Managementの推奨ルールとして、constを使う事が定義されています。それは、Reactがコンポーネントを再レンダーした際に新しい値をconstの変数に読み込むことになるからです。それ以外の時に変数が変わってしまってもReactは理解することができません。ですのでその問題を防ぐためにもconstを使うことをお勧めします。

状態の変化を理解する

では状態の変化を理解するために、同じ問題を別のコードで再現してみます。

export default function App() {
  function getNum() {
    let num = 32;
    return num;
  }

  let myNum = getNum();
  myNum = 0;

  let newNum = getNum();

  return (
    <div className="App">
      <div>myNum: {myNum}</div>
      <div>newNum: {newNum}</div>
    </div>
  );
}

オリジナルのnewNumはそのまま32と表示されますが、上書きした方のmyNumは0に変更されたものが表示されました。

オブジェクトの場合

export default function App() {
  let num = {
    id: 5,
  };
  
  function getNum() {
    return num;
  }

  let myNum = getNum();
  myNum.id = 0;

  let newNum = getNum();

  return (
    <div className="App">
      <div>myNum: {myNum.id}</div>
      <div>newNum: {newNum.id}</div>
      {num.id}
    </div>
  );
}

オブジェクトを上書きした場合は、元のオブジェクトまで変更されてしまいました。

これは、JavaScriptの特徴でプリミティブのデータ(String、Number、Booleanなど)の場合はデータのコピーを返し、Array(配列)やオブジェクトの場合はReference(参照)できるデータを返します。つまり、オリジナルのデータという事になります。

このコンセプトを理解することが重要になります。

ではすべてオブジェクトにしてしまえばよいのでは?

そう思って、下記の様にオブジェクトにしたところ、Reactがcountの状態が変わったことを察知できなかったので実際にデータが変わったとしてもRe-render(画面の更新)が行われませんでした。

import { useState } from "react";

export default function App() {
  let [count, setCount] = useState({
    key:1,
    num:10
  });

  function addOne() {
    count.num++;
    console.log(count.num)
  }
  return (
    <div className="App">
      <button onClick={addOne}>Count = {count.num}</button>
    </div>
  );
}



このようにコンソールには正しくcountの値が更新されていますが、Reactではレンダーされません。

そういう事なので、useState()のsetterを使ってuseState()で設定した値を更新すること必須であることが分かりました。

これは、useStateのsetterを使わないとReactがState Managementで管理しているデータの変化を察知できず、コンポーネントの再レンダーができないからです。

Setterを使ってstateを更新する

では今までに学習したことをもとに、useStateについてくるSetter(useStateの2番目にあるメソッド)を使ってcountを更新します。

import { useState } from "react";

export default function App() {
  const [count, setCount] = useState(10);

  function addOne() {
    setCount(count + 1);
  }
  return (
    <div className="App">
      <button onClick={addOne}>Count = {count}</button>
    </div>
  );
}

これでボタンを押したときに関数が変わり、Reactが最新の状態に合わせてレンダー(読み込み)してくれました。

prevStateを使った状態管理で解決できること

前回までに記載した現在の状態を操作するsetCount(count + 1);の記載は間違っています。

今までのコードに問題がありませんでしたが、下記の例のように将来的に大きな間違いを起こすこともあり得ます。

ここでは、count + 1を行うincrementという関数があり、incrementDoubleの関数で2回発火しているのでボタンを押すたびに2の数が足されるように設定されています。しかし、実際にボタンを押してみると1しか足されません。

なぜでしょうか?

import { useState } from "react";

function useCounter() { 
  const [count, setCount] = useState(10S); 
  const increment = () => setCount(count + 1); 
  const decrement = () => setCount(count - 1); 
  return { count, increment, decrement }; 
};

export default function App() { 
  const { count, increment, decrement } = useCounter(10); 
  const incrementDouble = () => { 
    increment(); 
    increment(); 
  }; 
  const decrementDouble = () => { 
    decrement(); 
    decrement(); 
  }; 
  return ( 
    <div className="App"> 
      <h1>Count: {count}</h1> 
      <button onClick={incrementDouble}>+2</button> 
      <button onClick={decrementDouble}>-2</button> 
    </div> 
  ); 
}

increment関数で先ほど書いたaddOneの関数にあるsetCount()ではcount+1と記載していることに注目してください。

これは、setCount(count + 1)でsetCountがcount(1) + 1 = 2 を2回行っているだけだからです。

ですので、setterを使用する際には現在のState、この場合はcountをいじるというよりも、PreviousState(状態が変わる前のデータ)を使用することが推奨されます。

prevStateはPreviousStateの略です。setterでは、prevStateのargumentsオブジェクトが与えられます。これはStateが変化する前のデータのことです。

では、上記のコードを下記の様に変えてみます。

const increment = () => setCount((prevState)=>prevState + 1);

const decrement = () => setCount((prevState)=>prevState – 1);

import { useState } from "react";

function useCounter() { 
  const [count, setCount] = useState(10); 
  const increment = () => setCount((prevState)=>prevState + 1); 
  const decrement = () => setCount((prevState)=>prevState - 1); 
  return { count, increment, decrement }; 
};

export default function App() { 
  const { count, increment, decrement } = useCounter(10); 
  const incrementDouble = () => { 
    increment(); 
    increment(); 

  }; 
  const decrementDouble = () => { 
    decrement(); 
    decrement(); 
  }; 
  return ( 
    <div className="App"> 
      <h1>Count: {count}</h1> 
      <button onClick={incrementDouble}>+2</button> 
      <button onClick={decrementDouble}>-2</button> 
    </div> 
  ); 
}

これで正しく、2ずつ足されるようになりました。

おまけ

再利用可能なコンポーネント

Reactの強みとして再利用可能なコンポーネントという概念があります。

このカウンターボタンも下記のように4か所で使用するとします。

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(10);

  function addOne() {
    setCount((prevValue)=>prevValue + 1);
  }
  return <button onClick={addOne}>Count = {count}</button>;
}

export default function App() {

  return (
    <div className="App">
      <Counter />
      <Counter />
      <Counter />
      <Counter />
    </div>
  );
}

そうすると、この画像のように独立してstate(状態)を管理した状態を保つことができることが確認できました。

まとめ

これでuseStateでなぜconstを使うべきなのか、またsetterの正しい使い方が理解できたでしょうか?

お疲れ様でした。