以下の公式ドキュメントに沿って勉強しました
https://ja.reactjs.org/docs/hooks-intro.html
フックとは
登場当初のReactはコンポーネントをclassとして書いて、値をclassのsteteやpropとして管理するというお作法でした。しかしながら、javascriptのthisの挙動が他の言語と違っていたり、ステートフルなコンポーネントの使いまわしがやりにくかったり、その結果としてコードが冗長になったり多層に積み重なってとにかく辛い、という事になっていたのを解消するべく、関数として書けるようにしたのがフックだそうです。
https://ja.reactjs.org/docs/hooks-intro.html
元々のコンポーネントやprops, state, コンテクスト, ref, ライフサイクルについてはそのまま使えるように設計されていて変わるのは書き方だけであり、フックを実装済みのコンポーネントについてはclassで書いていたものと同じものをフックでも作れるし、その方が良いよという事のようです。
https://ja.reactjs.org/docs/hooks-faq.html
1. プロジェクトの作成
npx create-react-app my-appで作った新規プロジェクトのApp.jsを書き換えてやってみます。
npx create-react-appについては以下に書いたので省略します
https://qiita.com/studio_haneya/items/539adda6df7b7c909da6
2. ステートフック
コンポーネントのstatesを使えるようにするフックです
classで書いた場合
App.jsを以下のように書き換えます。constructorでthis.stateを定義して、button onClickでthis.state.countを1つずつ増加していくという書き方でした。
import React from 'react';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
export default App;
フックで書く場合
上記をフックで書き換えると以下のようになります。useState()がconstructorの代わりをしてくれる関数で、const [count, setCount] = useState(0)と書くと、countという名前のstateの初期値を0として、setCount関数により更新しますという宣言になります。この初期値の代入はApp()が最初に呼ばれた時にしか実行されず、2回目以降では既に定義済みのcountというstateの値を参照するだけで、これをuseState()が上手くやってくれるわけです。
import React, { useState } from 'react';
function App() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default App;
2. 副作用(side-effect)フック
コンポーネント内部から外部データを取得する動作をReactでは副作用(side-effect)または短く作用(effect)と呼んでいるようです。
2-1. クリーンアップが不要な場合
例えば先程の例のようにbutton onClickで増えていくカウントを、本文中だけでなくサイトタイトルにも書き出したい場合、render()される度にdocument.titleを更新したくなります。
クラスで書く場合
これをクラスで書く場合、render()の度に呼ばれるクラスコンポーネントは存在しない為、以下のようにcomponentDidMount()とcomponentDidUpdate()に同じ内容を書くことになります。
import React from 'react';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
export default App;
副作用フックで書く場合
react.useEffect()内に書くことで副作用を仕込むことができます。useEffect()はカスタマイズしない限り初回のrender()時と更新がかかって再render()するときに実行されるので、更新がかかったらついでに何かをしたいときにうってつけのやり方になります。
import React, { useState, useEffect } from 'react';
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default App;
2-2. クリーンナップが必要な場合
外部データを読みに行っているような場合はメモリリークが起こらない安全なやり方としてクリーンアップするやり方が推奨されています。これをフックで書く場合はクリーンアップする為の関数をuseEffect()の返り値にすると自動的に処理してくれます。
以下ではChatAPIという外部データ取得用のクラスが既に書かれている場合に、ChatAPI.subscribeToFriendStatus()で取得した値をChatAPI.unsubscribeFromFriendStatus()によりクリーンアップしています。
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
副作用が実行される度にクリーンアップも実行される為、render()される度に実行されることになり、パフォーマンス上の問題が生じる可能性があります。その場合には、更新した場合に副作用の実行が必要な値を監視し、値の変化がない場合は副作用を実行しない、という形で負荷を低減することが出来ます。
https://ja.reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
以下ではcountが更新されていない場合に副作用が実行されないようにしています
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
3. その他フックAPI
useState、useEffect以外にもいろいろなフックAPIが用意されています
https://ja.reactjs.org/docs/hooks-reference.html
基本のフック
- useState
- useEffect
- useContext
追加のフック
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
4. フックのルール
Reactフックが正常に機能する為には以下の2つのルールがあります
https://ja.reactjs.org/docs/hooks-rules.html
4-1. Reactの関数のトップレベルから呼ぶ
ループや条件分岐、あるいはネストされた関数内で呼び出してはいけない
4-2. Reactの関数から呼ぶ
通常のJavascript関数から呼んではいけない
(Reactの関数から呼び出すか、カスタムフックから呼ぶ)
eslint
上記をチェックできるようにしたeslintプラグインが公開されていて、create-react-appすると自動で適用されるようになっているようです。
https://ja.reactjs.org/docs/hooks-rules.html
5. カスタムフック
2-2ではAPIからユーザー情報を取得してOnlineなのかOfflineを返す関数を書いていましたが、これを1つのフックとしておいて使い回すことが出来ます。
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
フックだと分かるようにuseStateやuseEffectのように、useから始まるカスタムフック名を設定するように推奨されています。
https://ja.reactjs.org/docs/hooks-custom.html
以下のように書けば同じロジックを複数のコンポーネントから使うことが出来ます。
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
6. まとめ
React用のライブラリのスニペットがフックで書かれていたり、フックに慣れてないとReactを使っていくのに支障が出るようになりつつあるようですので頑張って覚えていきましょう。