• Home
  • React入門 - React Hooksを理解しよう

React入門 - React Hooksを理解しよう

React Hooksとは

まず、React Hooksがどういうものかについて簡単に説明したいと思います。 React Hooksとはクラスを書かなくても関数だけでReactのコンポーネントを定義するための仕組みです。

以前のReactではクラスベースで以下のようにコンポーネントを定義していました。

import { Component } from "react"

class MyComponent extends Component {

  constructor(props) {
    super(props)
    this.state = {
      items: []
    }
  }

  async componentDidMount() {
    const res = await fetch('/api/items')
    const items = await res.json()
    this.setState({
      items
    })
  }

  render() {
    const { items } = this.state;
    return (
      <ul className="list">
        {items.map(item => <li key={item.id}>{item.label}</li>)}
      </ul>
    )
  }
}

クラスの書き方でもいいのですが、クラスで書く分余計な記述が増え、少し冗長に見えたり、またコンパイル時に最適化されない問題があります。 Hooksを使った関数ベースで書くと以下のように書けます。

import { useState, useEffect } from "react"

const MyComponent = () => {
  const [items, setItems] = useState([]);
  useEffect(() => {
    fetch('/api/items')
      .then((res) => res.json)
      .then((items) => setItems(items))
  }, []);
  return (
    <ul className="list">
      {items.map(item => <li key={item.id}>{item.label}</li>)}
    </ul>
  )
}

少し記述が楽になりましたね。

Hooksのための関数

次に、Hooksを扱うにあたって覚えておくべき関数について学習しましょう。 先ほどの例でも出てきましたが、useStateuseEffectは関数でコンポーネントを記述するために覚えておきたいHooks関数です。 その他にもいくつかのHooksがあります。 順番に見ていきましょう。

useState

useStateはコンポーネントの状態を管理するためのHooksです。例えば、以下はuseStateを使ったカウンターです。

const Counter = () => {
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount(count + 1)
  }

  return (<div>
    {count}回クリックされました。
    <button onClick={handleClick}>クリック</button>
  </div>)
}

useStateは以下のように、初めの状態をuseStateに代入し、結果を配列として受け取ります。 配列の一番初めに状態が入っています。状態更新用関数によって更新されるとここの状態が更新されます。

const [状態、状態更新用関数] = useState(初めの状態)

今回の例では、[count, setCount]として受け取っているのでcountが状態、setCountがcountを更新するための関数となっています。 buttonがクリックされた際にonClickにアサインされたhandleClickが動作します。 handleClickは、setCount(count + 1)となっているので、現在のcountに1を足した値がcountの新たな状態になります。 setCountが実行されると、Counter関数が再評価されreturnされるJSXの中身が更新されます。 初めてクリックをした場合には「1回クリックされました」と表示されます。

useEffect

副作用を扱うための関数です。ここでいう副作用とは例えば外部APIと通信するとかページのタイトルを書き換えるなど、stateの更新ではなく外部と影響を及ぼし合うことを指します。 また、useEffectを使うことで通信回数を減らすというメリットもあります。

例えば以下の書き方だとコンポーネントが評価されるたびに通信が発生してしまいます。

const ListFetcher = () => {
  const [items, setItems] = useState([])
  // useEffectを使っていないと何回もfetchが実行されてしまう...
  fetch('/api/items')
    .then((res) => res.json)
    .then((items) => setItems(items))

  return (
    <ul className="list">
      {items.map(item => <li key={item.id}>{item.label}</li>)}
    </ul>
  )
}

useEffectで囲ってあげることによって、初めの一回だけ実行されます。

useEffect(() => {
  fetch('/api/items')
    .then((res) => res.json)
    .then((items) => setItems(items))
}, []);

ここで、useEffectの第2引数に代入されている[]が気になりませんか?

これはuseEffectを再び実行するかどうかを判断するために使われます。[]の中身に代入された値が更新されるたびにuseEffectが再実行されます。 以下は、idを受け取るたびにAPIを際実行するサンプルになります。

const ListFetcher = ({ id }) => {
  const [items, setItems] = useState([])

  useEffect(() => {
    fetch(`/api/${id}/items`)
      .then((res) => res.json)
      .then((items) => setItems(items))
  }, [id])

  return (
    <ul className="list">
      {items.map(item => <li key={item.id}>{item.label}</li>)}
    </ul>
  )
}

以下の例ではセレクターで値を選択し直すことで、再度ListFetcherがAPIを実行して再描画します。

const Main = () => {
  const [id, setId] = useState('xxxx')

  return (<div>
    <select
      onChange={(e) => {
        setId(e.target.value)
      }}
    >
      <option value="xxxx">xxxx</option>
      <option value="yyyy">yyyy</option>
      <option value="zzzz">zzzz</option>
    </select>
    <ListFetcher id={id} />
  </div>)
}

useRef

さらにここからは踏み込んでReactで標準で用意されている他のhooksについても紹介しましょう。 まずはuseRefです。 useRefは再描画は防止しつつ、値を更新したい場合に利用できます。 以下の例を見てみましょう。

const Timer = ({ time }) => {
  const [second, setSecond] = useState(0);
  const ref = useRef();
  useEffect(() => {
    ref.current = 0;
    setInterval(() => {
      ref.current += 1
    }, 1000)
  }, [])

  const handleClick = () => {
    setSecond(ref.current)
  }

  return (
    <div>
      {second > 0 && (<div>{second} 秒経過</div>)}
      <button onClick={handleClick}>ストップ</button>
    </div>
  )
}

以下の例では、useRefを使ってimmutable(書き換え可能)な変数を用意しています。 useRefから生成された、refのcurrentをsetIntervalの度に更新することでTimer関数を再実行することなく、 ref.currentの値だけを更新することが可能です。

handleClickによって、setSecond されることによって初めてTimer関数が再実行され、ストップされるまでの時間が「3秒経過」のような形で描画されます。

useMemo

複雑な計算結果の答えが必要な場合に、何度も関数が実行されるたびに計算し直さなくてもいいように計算結果をキャッシュしておくのに便利な関数です。 例えば以下のように何らかのクラスを初期化する場合にも使えます

const result = useMemo(() => {
  const instance new SomeClass(name)
  return instance;
}, [name])

再度useMemoが実行された時に、nameの値が一致していれば、そのnameを使って得たインスタンスがキャッシュされているので、 useMemoの中のcallbackは実行されず、キャッシュが返されます。 ただし、nameの値が更新されている場合は、callbackが実行されそのnameに対する新たなキャッシュが作られます。

useCallback

最後にuseCallbackを紹介します。useCallbackは描画領域が大きかったり計算コストがでかいコンポーネントにコールバック関数を渡す際に propsの更新によって再描画されることを防ぐために存在します。 例えば、以下のuseCallbackを利用していないコンポーネントだとコールバック関数が実行される度に作られてしまうのでMassiveComponentがまた再計算されることになってしまいます。

const Main = ({ className }) => {
  const handleChange = () => {
    // ここに処理
  }

  return (<div className={className}>
    <MassiveComponent
      onChange={handleChange}
    />
  </div>)
}

以下のようにuseCallbackを使うことで一度定義した関数をキャッシュし、MassiveComponentの再計算を防ぎます

const Main = ({ className }) => {
  const handleChange = useCallback(() => {
    // ここに処理
  }, [])

  return (<div className={className}>
    <MassiveComponent
      onChange={handleChange}
    />
  </div>)
}

まとめ

これら紹介したHooksを利用すれば以前のクラスを使った書き方をしなくても関数ベースでコンポーネントを定義することができます。 関数ベースに記述することでコード量も減らせますし、コンパイルする際にコードが最適化されます。 最初は、useState, useEffect, useRefを覚えておくだけでも十分にコンポーネント開発ができることでしょう。 ただし、複雑なコンポーネントを作っていて妙に描画が遅いとかチラつくことが出てくればパフォーマンスを意識してuseMemouseCallbackといったhooksも使ってみてください。

あわせて読みたい

React入門 - Reactで開発できる環境を整備するまで

2021.09.24

学習状況

ログインすることで学習状況を確認できます