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を扱うにあたって覚えておくべき関数について学習しましょう。
先ほどの例でも出てきましたが、useState
、useEffect
は関数でコンポーネントを記述するために覚えておきたい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
を覚えておくだけでも十分にコンポーネント開発ができることでしょう。
ただし、複雑なコンポーネントを作っていて妙に描画が遅いとかチラつくことが出てくればパフォーマンスを意識してuseMemo
やuseCallback
といったhooksも使ってみてください。