hooks-pattern について
関数コンポーネントで状態やライフサイクルを扱い、再利用可能な状態ロジックを切り出すReact Hooksの開発ガイドです。コンポーネントのロジックを整理し、クラスコンポーネントへの依存を減らしたいReact開発者向けです。
主な機能
- 01useStateによるコンポーネント状態の管理
- 02useEffectによる副作用の処理
- 03カスタムフックの作成と再利用
- 04Hooksのルールの遵守
- 05不要なuseEffectの回避
活用シーン
- 01関数コンポーネントへの状態機能の追加
- 02複数コンポーネント間での状態ロジックの再利用
- 03クラスコンポーネントから関数コンポーネントへのリファクタリング
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ patternsdev/ skills --skill hooks-patternインストール後、Claude Code で/hooks-patternを入力して読み込まれたか確認します。
詳細
Hooks パターン
目次
React 16.8 では、Hooks と呼ばれる新機能が導入されました。Hooks を使うと、ES2015 のクラスコンポーネントを使わずに、React の状態やライフサイクルメソッドを利用できます。
Hooks は必ずしもデザインパターンではありませんが、アプリケーションの設計において非常に重要な役割を果たします。従来のデザインパターンの多くは、Hooks で置き換えることができます。
使用する場面
- 関数コンポーネントに状態やライフサイクルの振る舞いを追加する必要がある場合に使用します
- 複数のコンポーネントで状態を持つロジックを抽出して再利用する場合に便利です
- よりすっきりとした、組み合わせやすいコードにするため、クラスコンポーネントの代わりに使用します
使い方
- 関数コンポーネントのローカル状態には
useStateを、副作用にはuseEffectを使用します - 再利用可能なロジックをカプセル化して共有するには、カスタムフック(
useで始まる名前)を作成します - Hooks のルールに従い、Hooks はトップレベルでのみ、React の関数内でのみ呼び出します
- 不要な
useEffectは避け、派生状態はコンポーネント本体で直接計算します - 可能な場合は、手動で
useMemo/useCallbackを使うのではなく、React Compiler にメモ化を任せます
詳細
クラスコンポーネント
Hooks が React に導入される前は、コンポーネントに状態やライフサイクルメソッドを追加するために、クラスコンポーネントを使う必要がありました。React の一般的なクラスコンポーネントは、次のようになります。
class MyComponent extends React.Component {
/* Adding state and binding custom methods */
constructor() {
super()
this.state = { ... }
this.customMethodOne = this.customMethodOne.bind(this)
this.customMethodTwo = this.customMethodTwo.bind(this)
}
/* Lifecycle Methods */
componentDidMount() { ...}
componentWillUnmount() { ... }
/* Custom methods */
customMethodOne() { ... }
customMethodTwo() { ... }
render() { return { ... }}
}
クラスコンポーネントには、コンストラクター内の状態、コンポーネントのライフサイクルに応じて副作用を実行する componentDidMount や componentWillUnmount などのライフサイクルメソッド、クラスに追加のロジックを加えるカスタムメソッドを含めることができます。
React Hooks の導入後もクラスコンポーネントを使うことはできますが、いくつかの欠点があります。クラスコンポーネントを使う際によくある問題を見ていきましょう。
ES2015 クラスを理解する
React Hooks が登場する前は、状態やライフサイクルメソッドを扱えるコンポーネントはクラスコンポーネントだけでした。そのため、追加の機能を加えるために、関数コンポーネントをクラスコンポーネントにリファクタリングしなければならないことがよくありました。
この例では、ボタンとして機能するシンプルな div を用意します。
function Button() {
return <div className="btn">disabled</div>;
}
常に disabled と表示するのではなく、ユーザーがボタンをクリックしたときに enabled に変え、その際にボタンに追加の CSS スタイルを適用したいとします。
そのためには、状態が enabled なのか disabled なのかを把握できるよう、コンポーネントに状態を追加する必要があります。つまり、関数コンポーネント全体をリファクタリングし、ボタンの状態を管理するクラスコンポーネントにする必要があります。
export default class Button extends React.Component {
constructor() {
super();
this.state = { enabled: false };
}
render() {
const { enabled } = this.state;
const btnText = enabled ? "enabled" : "disabled";
return (
<div
className={`btn enabled-${enabled}`}
onClick={() => this.setState({ enabled: !enabled })}
>
{btnText}
</div>
);
}
}
この例ではコンポーネントがとても小さいため、リファクタリングは大きな手間ではありません。しかし、実際のコンポーネントにはもっと多くのコードが含まれていることが多く、リファクタリングがはるかに難しくなります。
リファクタリング中に誤って振る舞いを変えないようにするだけでなく、ES2015 クラスの仕組みを理解する必要もあります。カスタムメソッドを bind する必要があるのはなぜでしょうか?constructor は何をするのでしょうか?this キーワードはどこから来るのでしょうか?データフローを誤って変えずにコンポーネントを適切にリファクタリングする方法を把握するのは、難しい場合があります。
再構成
複数のコンポーネント間でコードを共有する一般的な方法は、Higher Order Component または Render Props のパターンを使用することです。どちらのパターンも有効で推奨される手法ですが、後からこれらのパターンを追加するには、アプリケーションを再構成する必要があります。
アプリを再構成しなければならないだけでなく、コンポーネントが大きくなるほど再構成は難しくなります。また、より深くネストされたコンポーネント間でコードを共有するために、多数のコンポーネントでラップすると、いわゆる ラッパー地獄 につながることがあります。開発者ツールを開くと、次のような構造が表示されることは珍しくありません。
<WrapperOne>
<WrapperTwo>
<WrapperThree>
<WrapperFour>
<WrapperFive>
<Component>
<h1>Finally in the component!</h1>
</Component>
</WrapperFive>
</WrapperFour>
</WrapperThree>
</WrapperTwo>
</WrapperOne>
ラッパー地獄 によって、アプリケーション内でデータがどのように流れているかを把握しづらくなり、予期しない動作が起きる理由を突き止めるのも難しくなります。
複雑さ
クラスコンポーネントにロジックを追加していくと、コンポーネントのサイズは急速に大きくなります。コンポーネント内のロジックが絡み合い、構造化されていない状態になると、クラスコンポーネント内の特定のロジックがどこで使われているかを開発者が理解しづらくなります。その結果、デバッグやパフォーマンスの最適化が難しくなることがあります。
ライフサイクルメソッドでは、コードの重複もかなり発生します。
コンポーネント自体は小さくても、その中のロジックはすでにかなり絡み合っていることがあります。一部は counter のロジックに固有であり、ほかの部分は width のロジックに固有です。コンポーネントが大きくなるにつれて、内部のロジックを構造化したり、関連するロジックを見つけたりすることが、ますます難しくなります。
ロジックが絡み合うだけでなく、ライフサイクルメソッド内でロジックを重複させています。componentDidMount と componentWillUnmount の両方で、ウィンドウの resize イベントに基づいてアプリの動作をカスタマイズしています。
Hooks
クラスコンポーネントが React の優れた機能とは限らないことは明らかです。クラスコンポーネントの使用時に React 開発者が直面しがちな一般的な問題を解決するため、React は React Hooks を導入しました。React Hooks は、コンポーネントの状態やライフサイクルメソッドを管理するために使用できる関数です。React Hooks を使うと、次のことが可能になります。
- 関数コンポーネントに状態を追加する
componentDidMountやcomponentWillUnmountなどのライフサイクルメソッドを使わずに、コンポーネントのライフサイクルを管理する- アプリ全体の複数のコンポーネント間で、同じ状態管理ロジックを再利用する
まず、React Hooks を使って関数コンポーネントに状態を追加する方法を見てみましょう。
State Hook
React には、関数コンポーネント内の状態を管理する useState という Hook が用意されています。
useState Hook を使って、クラスコンポーネントを関数コンポーネントに再構成する方法を見てみましょう。ここでは、入力フィールドを表示するだけの Input というクラスコンポーネントを使います。ユーザーが入力フィールドに何か入力すると、state 内の input の値が更新されます。
class Input extends React.Component {
constructor() {
super();
this.state = { input: "" };
this.handleInput = this.handleInput.bind(this);
}
handleInput(e) {
this.setState({ input: e.target.value });
}
render() {
<input onChange={handleInput} value={this.state.input} />;
}
}
useState Hook を使うには、React が提供する useState メソッドを利用します。useState メソッドは引数を受け取ります。この引数は state の初期値で、この場合は空の文字列です。
useState メソッドから次の2つの値を分割代入できます。
- state の現在の値。
- state を更新するためのメソッド。
const [value, setValue] = React.useState(initialValue);
1つ目の値は、クラスコンポーネントの this.state.[value] に相当します。
2つ目の値は、クラスコンポーネントの this.setState メソッドに相当します。
入力値を扱うため、state の現在の値を input、state を更新するメソッドを setInput と呼ぶことにします。初期値は空の文字列にします。
const [input, setInput] = React.useState("");
これで、Input クラスコンポーネントを状態を持つ関数コンポーネントにリファクタリングできます。
function Input() {
const [input, setInput] = React.useState("");
return <input onChange={(e) => setInput(e.target.value)} value={input} />;
}
input フィールドの値は、クラスコンポーネントの例と同様に、input state の現在の値と等しくなります。ユーザーが入力フィールドに入力すると、setInput メソッドを使って input state の値がそれに応じて更新されます。
Effect Hook
関数コンポーネント内の状態を管理するには、useState コンポーネントを使用できることを見てきました。一方、クラスコンポーネントのもう一つの利点は、コンポーネントにライフサイクルメソッドを追加できることでした。
useEffect フックを使うと、コンポーネントのライフサイクルに「フック」できます。useEffect フックは、componentDidMount、componentDidUpdate、componentWillUnmount の各ライフサイクルメソッドを実質的に組み合わせたものです。
componentDidMount() { ... }
useEffect(() => { ... }, [])
componentWillUnmount() { ... }
useEffect(() => { return () => { ... } }, [])
componentDidUpdate() { ... }
useEffect(() => { ... })
State Hook のセクションで使った入力の例を使ってみましょう。ユーザーが入力フィールドに何か入力するたびに、その値もコンソールに記録したいとします。
input の値を「監視」する useEffect フックを使う必要があります。useEffect フックの 依存配列 に input を追加すれば実現できます。依存配列は、useEffect フックが受け取る第 2 引数です。
useEffect(() => {
console.log(`The user typed ${input}`);
}, [input]);
これで、ユーザーが値を入力するたびに、その値がコンソールに記録されます。
カスタムフック
React が提供する組み込みフック(useState、useEffect、useReducer、useRef、useContext、useMemo、useImperativeHandle、useLayoutEffect、useDebugValue、useCallback)に加えて、独自のカスタムフックも簡単に作成できます。
すべてのフックが use で始まることに気付いたかもしれません。React が フックのルールに違反していないか確認できるよう、フック名は use で始めることが重要です。
入力中にユーザーが押す特定のキーを追跡したいとします。カスタムフックは、対象とするキーを引数として受け取れる必要があります。
function useKeyPress(targetKey) {
const [keyPressed, setKeyPressed] = React.useState(false);
function handleDown({ key }) {
if (key === targetKey) {
setKeyPressed(true);
}
}
function handleUp({ key }) {
if (key === targetKey) {
setKeyPressed(false);
}
}
React.useEffect(() => {
window.addEventListener("keydown", handleDown);
window.addEventListener("keyup", handleUp);
return () => {
window.removeEventListener("keydown", handleDown);
window.removeEventListener("keyup", handleUp);
};
}, []);
return keyPressed;
}
キー入力のロジックを Input コンポーネント内に置く代わりに、同じロジックを何度も書き直すことなく、複数のコンポーネントで useKeyPress フックを再利用できるようになりました。
フックのもう一つの大きな利点は、コミュニティがフックを作成して共有できることです。コミュニティが作成したフックを一覧で紹介しており、アプリケーションですぐに使えるウェブサイトをいくつか紹介します。
カウンターと幅の例を React Hooks を使って書き直すと、App 関数のロジックをいくつかの部分に分けられます。
useCounter:countの現在の値と、incrementメソッドおよびdecrementメソッドを返すカスタムフックです。useWindowWidth:ウィンドウの現在の幅を返すカスタムフックです。App:CounterコンポーネントとWidthコンポーネントを返す、関数型のステートフルコンポーネントです。
クラスコンポーネントの代わりに React Hooks を使うことで、ロジックを分割し、ロジックを切り離した、より小さく再利用可能な部品にできました。
React Hooks を使うことで、コンポーネントのロジックを複数の小さな部品に 分離 しやすくなりました。同じステートフルなロジックの 再利用 がずっと簡単になり、コンポーネントに状態を持たせるために関数コンポーネントをクラスコンポーネントに書き換える必要もなくなりました。ES2015 クラスに関する深い知識は不要になり、ステートフルなロジックを再利用できることで、コンポーネントのテスト容易性、柔軟性、可読性が向上します。
Hook に関する追加のガイダンス
他のコンポーネントと同様に、作成したコードに Hooks を追加する際に使う特別な関数があります。よく使われる Hook 関数をいくつか簡単に紹介します。
useState
useState Hook を使うと、クラスコンポーネントに変換することなく、関数コンポーネント内の状態を更新および操作できます。この Hook の利点の一つは、シンプルで、他の React Hooks ほど複雑ではないことです。
useEffect
useEffect フックは、関数コンポーネントの主要なライフサイクルイベント中にコードを実行するために使用します。関数コンポーネントの本体では、ミューテーション、サブスクリプション、タイマー、ログ出力などの副作用を実行できません。これらを許可すると、UI にわかりにくいバグや不整合が生じる可能性があります。useEffect フックはこうした「副作用」を防ぎ、UI がスムーズに動作するようにします。これは、componentDidMount、componentDidUpdate、componentWillUnmount を1か所にまとめたものです。
useContext
useContext フックは、React.createContext が返す値であるコンテキストオブジェクトを受け取り、そのコンテキストの現在の値を返します。また、useContext フックは React Context API と連携し、アプリ内のさまざまな階層を通じてアプリの props を渡さなくても、データを共有できます。
useContext フックに渡す引数はコンテキストオブジェクトそのものでなければならない点に注意してください。また、useContext を呼び出すコンポーネントは、コンテキストの値が変わるたびに必ず再レンダリングされます。
useReducer
useReducer フックは setState の代替手段であり、複数のサブ値を扱う複雑な状態ロジックがある場合や、次の状態が前の状態に依存する場合に特に適しています。reducer 関数と初期状態を受け取り、配列の分割代入によって、現在の状態と dispatch 関数を返します。useReducer は、深い階層の更新を引き起こすコンポーネントのパフォーマンスも最適化します。
メリット
コード行数の削減
フックを使うと、ライフサイクルごとではなく、関心事や機能ごとにコードをまとめられます。そのため、コードが読みやすく簡潔になるだけでなく、行数も少なくなります。以下では、React を使った検索可能な商品データテーブルのシンプルな状態を持つコンポーネントと、useState キーワードを使ったフックでの書き方を比較します。
状態を持つコンポーネント
class TweetSearchResults extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: "",
inThisLocation: false,
};
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
this.handleInThisLocationChange =
this.handleInThisLocationChange.bind(this);
}
handleFilterTextChange(filterText) {
this.setState({
filterText: filterText,
});
}
handleInThisLocationChange(inThisLocation) {
this.setState({
inThisLocation: inThisLocation,
});
}
render() {
return (
<div>
<SearchBar
filterText={this.state.filterText}
inThisLocation={this.state.inThisLocation}
onFilterTextChange={this.handleFilterTextChange}
onInThisLocationChange={this.handleInThisLocationChange}
/>
<TweetList
tweets={this.props.tweets}
filterText={this.state.filterText}
inThisLocation={this.state.inThisLocation}
/>
</div>
);
}
}
フックを使った同じコンポーネント
const TweetSearchResults = ({ tweets }) => {
const [filterText, setFilterText] = useState("");
const [inThisLocation, setInThisLocation] = useState(false);
return (
<div>
<SearchBar
filterText={filterText}
inThisLocation={inThisLocation}
setFilterText={setFilterText}
setInThisLocation={setInThisLocation}
/>
<TweetList
tweets={tweets}
filterText={filterText}
inThisLocation={inThisLocation}
/>
</div>
);
};
複雑なコンポーネントを簡素化
JavaScript のクラスは管理が難しく、ホットリロードとの相性も悪く、圧縮も効きにくい場合があります。React フックはこれらの問題を解決し、関数型プログラミングを簡単に実現します。フックを導入すれば、クラスコンポーネントは不要です。
状態を持つロジックの再利用
JavaScript のクラスは、継承階層を重ねることを促し、全体的な複雑さやエラーの可能性をすぐに増大させます。一方、フックを使えば、クラスを記述せずに状態やその他の React の機能を利用できます。React では、コードを何度も書き直さなくても、状態を持つロジックを再利用できます。これによりエラーの可能性が減り、プレーンな関数で構成できるようになります。
表示に関わらないロジックの共有
フックが導入されるまで、React には表示に関わらないロジックを抽出して共有する方法がありませんでした。その結果、共通の問題を解決するためだけに、HOC パターンやレンダープロパティなど、複雑さが増す手法が使われるようになりました。しかし、フックの導入により、状態を持つロジックをシンプルな JavaScript 関数に抽出できるようになり、この問題は解決されました。
もちろん、フックには注意しておくべき潜在的なデメリットもあります。
- ルールを守る必要があります。リンタープラグインがないと、どのルールに違反したのかを把握するのは困難です。
- 適切に使えるようになるには、十分な練習が必要です(例:useEffect)。
- 誤った使い方に注意が必要です(例:useCallback、useMemo)。
React Hooks とクラス
React に Hooks が導入されたことで、新たな疑問が生まれました。Hooks を使う関数コンポーネントとクラスコンポーネントは、どのように使い分ければよいのでしょうか。Hooks を使えば、関数コンポーネントでも state や一部のライフサイクル Hooks を利用できます。また、クラスを書かずにローカル state やその他の React の機能を使うこともできます。
使い分けの参考として、Hooks とクラスの違いを以下に示します。
| React Hooks | クラス |
|---|---|
| 複数の階層を避け、コードをより明確にできます | 一般に、HOC または renderProps を使う場合、DevTools で確認しようとすると、App を複数の階層で構成し直す必要があります |
| React コンポーネント間で一貫性を保てます。 | 関数のバインディングや呼び出されるコンテキストを理解する必要があるため、クラスは人にも機械にもわかりにくくなります。 |