sagantaf

メモレベルの技術記事を書くブログ。

React Hooksの基本とuseState

Hooksとは

HooksはReactの16.8.0バージョンから追加された新しい機能。

Hooksを使うことで、ReactのComponentを関数で定義することできる。従来はクラスによる定義のみであったが、関数でも定義できるようになったことで、

  • 同等の機能を実装する場合、クラスで実装するよりもコード量が少なくなる
  • ロジックを分離できるため再利用やテストがしやすい

といったメリットを得られる。

仕組み

フックに関するよくある質問 – React

  • Reactはどのコンポーネントが現在レンダー中なのかを把握しており、メモリとして保持している。
  • また、Reactはフックが呼ばれる順番をメモリとして保持している。そのため、フックとコンポーネントを紐付けて管理できている。

注意点

  • フックはトップレベルでのみ定義すべき。上記の通り、フックが呼ばれる順番をReactは覚えるため、ループやif文の中で定義すると条件によっては順番が変わってしまい、エラーやおかしな挙動の原因になる。もし必要ならが、useEffectの中でループやif文を実行する、という回避策をとる。
  • Reactの関数コンポーネントの中でのみ定義すべき。

上記を2点をチェックできるlinterツールがあるので使う。create react appにはデフォルトで含まれている。インストールする場合は npm install eslint-plugin-react-hooks —-save-devを実行する。 eslint-plugin-react-hooks - npm

ESLint confファイルの事例

{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

useState

useStateの基本

書き方は、const [変数名, 更新関数名] = useState(初期値)

const [hogeNum, setHogeNum] = useState(0);
const [gehoStr, setGehoStr] = useState("hoge");

setHogeNum(10) // hogeNumに 10 に更新される
setGehoStr('geho') // gehoStrが 'geho' に更新される

更新関数に値を渡す → stateが渡された値に更新される

更新関数に関数を渡す → その関数の引数にstateの現在の値が入り、returnで渡された値が新たなstateになる。以下事例。currentCountに現在のcountの中身が格納される。

import React, { useState } from "react";

export default function App() {
  const [count, setCount] = useState(10);
  const decrement = () => {
    setCount((currentCount) => currentCount - 1);
  }
  const increment = () => {
    setCount((currentCount) => currentCount + 1);
  }

  return (
    <>
      <p>Count: {count}</p>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </>
  )
}

setCount((currentCount) => currentCount - 1); ではなく、setCount(count - 1);でも同等の結果になる。しかし、関数を渡した方が、stateを更新するロジックが外部に依存しない形になる。

こうすることで、更新するロジックが長くなって読みにくくなった場合でも、関数ごと外出しすることで読みやすさを保つことができる。

オブジェクトやリストもstateとして扱える

数値や文字列だけではなく、オブジェクト型やリスト型も利用できる。

  • オブジェクト型の例

更新メソッドを利用するときには、スプレッド演算子を使って更新する必要がある(後述の「いつ再レンダーされるか」を参照)。

import React, { useState } from "react";

export default function App() {
  const [count, setCount] = useState({ men: 0, women: 0 })

  const comingMen = () => {
    setCount((currentCount) => ({ ...currentCount, men: currentCount.men + 1 }));
  }
  const comingWomen = () => {
    setCount((currentCount) => ({ ...currentCount, women: currentCount.women + 1 }))
  }

  return (
    <>
      <p>Men: {count.men}</p>
      <p>Women: {count.women}</p>
      <button onClick={comingMen}>Men</button>
      <button onClick={comingWomen}>Women</button>
    </>
  )
}
  • リスト型の例

追加はオブジェクト型と同じくスプレッド構文を使って更新できるが、削除は色々方法がある。ここではfilterを利用している。

import React, { useState } from "react";

export default function App() {
  const [years, setYears] = useState([2019, 2020, 2021])

  const addYear = () => {
    setYears((current) => ([...current, current[current.length - 1] + 1]));
  }

  const deleteYear = () => {
    setYears(years.filter((_, i) => i != years.length - 1))
  }

  return (
    <>
      <p>Years: {years.map((year) => year + ',')}</p>
      <button onClick={addYear}>add year</button><button onClick={deleteYear}>del year</button>
    </>
  )
}

再レンダーされるタイミング

オブジェクト型のstateを更新するときに、以下のような書き方をしてしまうと再レンダーされない(上記の例の一部を抜粋)。

const comingMen = () => {
  count.men = count.men + 1;
  setCount(count);
}

公式ページ(フック API リファレンス – React)にあるように、ReactではObject.jsによる比較アルゴリズムを利用している。 たとえば

const hoge = { num: 1 };
const geho = { num: 1 };
console.log(Object.is(hoge, hoge)); // trueになるため同一とみなされる
console.log(Object.is(hoge, geho)): // 値は同じでもfalseになる

つまり、オブジェクトが同じであればtrueになり、別であればfalseになる。

count.men = count.men + 1としただけでは、countオブジェクトは同一であり何も変わっていないとみなされるため、再レンダーされない。 newCount = { ...count, men: count.men + 1 }のようにスプレッド演算子を利用して新しいオブジェクトを生成すれば別のオブジェクトとしてみなされ、再レンダーが実行される。

リスト型の場合でも、list.push()list.splice()を使うとオブジェクトが変化していないと判断されてしまうため注意。

参考

基礎から学ぶ React/React Hooks

基礎から学ぶ React/React Hooks

Amazon