はい、承知いたしました。「React FCとは? 関数コンポーネントの基本と使い方を解説」というテーマで、約5000語の詳細な記事を執筆します。記事の内容を直接表示します。
React FCとは? 関数コンポーネントの基本と使い方を徹底解説
Reactを使ったモダンなフロントエンド開発において、コンポーネントはUIを構築するための基本的な要素です。そして現在、そのコンポーネントの記述形式として主流となっているのが「関数コンポーネント」です。かつては「ステートを持たないただの関数」として扱われていましたが、React Hooksの登場により、関数コンポーネントはステートや副作用といったリッチな機能を扱えるようになり、開発の中心となりました。
本記事では、この「関数コンポーネント」(しばしば「React FC」とも呼ばれます)に焦点を当て、その基本的な概念から、Propsの扱い、そしてReact開発に不可欠な「Hooks」を使った状態管理や副作用の処理まで、詳細かつ網羅的に解説します。また、TypeScriptでの型定義や、パフォーマンス最適化のヒント、さらにはクラスコンポーネントとの比較にも触れ、関数コンポーネントを深く理解するための知識を提供します。
React開発初心者の方から、改めて関数コンポーネントとHooksの使い方を体系的に学びたい経験者の方まで、幅広く参考にしていただける内容を目指します。
1. はじめに:Reactの進化とコンポーネント
1.1 Reactにおけるコンポーネントの役割
Reactは、ユーザーインターフェース(UI)を構築するためのJavaScriptライブラリです。Reactの核心的な考え方の一つに「コンポーネント指向」があります。コンポーネントとは、UIを再利用可能な独立した小さな部品に分割したものです。例えば、ウェブサイトのヘッダー、フッター、ボタン、リストの各項目などは、それぞれ独立したコンポーネントとして考えることができます。
コンポーネント指向には、以下のようなメリットがあります。
- 再利用性: 一度作成したコンポーネントは、アプリケーション内の様々な場所で繰り返し使用できます。これにより、コードの重複を減らし、開発効率を向上させます。
- 保守性: UIが小さな部品に分割されているため、特定の機能や見た目を変更する際に、影響範囲が限定され、修正が容易になります。
- 開発効率: チーム開発において、各開発者が異なるコンポーネントを並行して開発できます。
- 可読性: コードが部品ごとに整理されるため、全体構造が把握しやすくなります。
Reactにおいて、コンポーネントは「Props」(プロパティ)を通じて親コンポーネントからデータを受け取り、自身の「State」(状態)を持つことでインタラクティブなUIを実現し、最終的にレンダリングするべきUIの構造を記述した「JSX」を返します。
1.2 クラスコンポーネントから関数コンポーネントへ
Reactが登場した当初、コンポーネントを記述する主な方法は「クラスコンポーネント」でした。クラスコンポーネントはES6のクラス構文を用いて記述され、React.Component
を継承します。ライフサイクルメソッド(componentDidMount
, componentDidUpdate
, componentWillUnmount
など)を使用してコンポーネントの生成、更新、破棄の各段階で処理を実行したり、this.state
でローカルな状態を管理したりすることができました。
一方で、ステートやライフサイクルメソッドを必要としないシンプルなコンポーネントは、単なるJavaScriptの関数として記述することが推奨されていました。これらは「ステートレス関数コンポーネント」(Stateless Functional Components – SFC)と呼ばれ、Propsを受け取ってJSXを返すだけの純粋な関数でした。SFCはクラスコンポーネントに比べて記述が簡潔であるという利点がありました。
しかし、SFCはステートを持つことができず、ライフサイクルメソッドに相当する機能もありませんでした。そのため、インタラクティブなUIや複雑なロジックを扱う必要がある場合は、記述が冗長になりがちなクラスコンポーネントを使わざるを得ませんでした。
この状況を劇的に変えたのが、React v16.8で導入された「React Hooks」です。Hooksは、関数コンポーネントにステートやライフサイクルに似た機能、およびその他のReactの機能を「フック」として「引っ掛ける」ことができるようにする仕組みです。Hooksの登場により、関数コンポーネントでもクラスコンポーネントと同等、あるいはそれ以上の表現力を持つことができるようになりました。その結果、関数コンポーネントがReact開発の主流となり、現在では新しいコンポーネントを作成する際は関数コンポーネントで記述することが強く推奨されています。
本記事で解説する「React FC」とは、まさにこのHooksによって強化された現代の「関数コンポーネント」を指します。
2. React FCとは? 関数コンポーネントの定義
React FC(Function Component)は、JavaScriptの関数として定義されるReactコンポーネントです。基本的な形は以下の通りです。
“`javascript
// 従来のfunctionキーワードを使った書き方
function MyComponent(props) {
// コンポーネントのロジック
return (
// レンダリングするJSX
);
}
// アロー関数を使った書き方(より一般的)
const MyComponent = (props) => {
// コンポーネントのロジック
return (
// レンダリングするJSX
);
};
“`
関数コンポーネントは以下の特徴を持ちます。
- 関数であること: ただのJavaScript関数として定義されます。特別なクラス構文や継承は不要です。
- Propsを引数として受け取る: 親コンポーネントから渡されるデータやコールバック関数は、関数の第一引数(慣習的に
props
という名前が使われます)としてまとめて渡されます。 - JSXを返す: コンポーネントがレンダリングするUIの構造は、JSXとして返されます。JSXはJavaScriptの構文を拡張したもので、HTMLのような見た目でUIを記述できます。
- Hooksを使うことでステートや副作用を扱える: Hooks (
useState
,useEffect
など) を利用することで、コンポーネント内で状態を保持したり、データ取得やDOM操作といった副作用を実行したりできます。
Hooksが登場する前の関数コンポーネントは、受け取ったPropsに基づいてUIを表示するだけの「ダムコンポーネント」や「プレゼンテーショナルコンポーネント」として使われることが多かったため、「Stateless Functional Components」と呼ばれていました。しかし、Hooksの登場により、関数コンポーネントでも自身でステートを持ち、ロジックを内包する「スマートコンポーネント」や「コンテナコンポーネント」の役割を担うことが可能になりました。そのため、「Functional Component」という呼び方がより適切であり、「FC」という略称も広く使われています。
3. React.FC
型注釈について (TypeScript)
Reactで開発する場合、近年ではTypeScriptを用いることが一般的です。TypeScriptを使うことで、静的な型チェックによるエラーの早期発見、コードの補完機能の向上、コードの意図を明確にするドキュメントとしての側面など、多くのメリットが得られます。
TypeScriptで関数コンポーネントを定義する際に、React.FC
という型注釈を見かけることがあります。
“`typescript
import React from ‘react’;
// Propsの型を定義
type MyComponentProps = {
name: string;
age?: number; // オプショナルなプロパティ
};
// React.FC 型注釈を使った関数コンポーネントの定義
const MyComponent: React.FC
return (
Hello, {name}!
{age &&
Age: {age}
}
);
};
“`
React.FC
(あるいはReact.FunctionalComponent
)は、関数コンポーネントであることを示す型注釈です。ジェネリクスとしてPropsの型を渡すことができます (React.FC<MyComponentProps>
)。
React.FC
を使うことにはいくつかの利点があります。
- 関数コンポーネントであることの明示: コードを読む人に関数コンポーネントであることが明確に伝わります。
- 戻り値の型チェック: 戻り値がReactNode(JSX要素、文字列、nullなど)であることが期待される型として保証されます。
- 暗黙的な
children
プロパティ: TypeScript 18より前のバージョンでは、React.FC
を使用すると、Propsの型定義に明示的にchildren
プロパティを含めなくても、自動的にchildren?: React.ReactNode
という型がPropsに追加されていました。
しかし、この「暗黙的なchildren
プロパティ」が後に問題視されるようになりました。
- 意図しない
children
の受け入れ:children
を受け取ることを想定していないコンポーネントでも、型チェック上はchildren
を受け入れ可能となってしまい、誤ったコンポーネントの使い方をしてもエラーにならない可能性があります。 - 型の不明瞭さ: 実際のPropsの型定義に
children
が含まれているかどうかが一見して分かりにくくなります。
そのため、TypeScript 18以降では、React.FC
を使わずに、以下のようにPropsの型を直接関数に適用し、必要であればPropsの型定義の中でchildren
を明示的に定義することが推奨されています。
“`typescript
import React, { ReactNode } from ‘react’;
// Propsの型を定義
type MyComponentProps = {
name: string;
age?: number;
// childrenを受け取る場合は明示的に定義
children?: ReactNode;
};
// Propsの型を直接関数に適用する(推奨される方法)
const MyComponent = ({ name, age, children }: MyComponentProps) => {
return (
Hello, {name}!
{age &&
Age: {age}
}
{children} {/ childrenを利用する場合 /}
);
};
“`
この方法の方が、コンポーネントが受け取るPropsの型がより明確になり、意図しないPropsの受け入れを防ぐことができます。したがって、現代のReact + TypeScript環境では、React.FC
を使用するよりもPropsの型エイリアスやインターフェースを定義し、それを関数引数に直接型注釈として適用するスタイルがベストプラクティスとされています。
4. 関数コンポーネントの基本構造と使い方
ここでは、Propsの受け渡し、JSXの返却、条件付きレンダリング、リストレンダリング、イベントハンドリングといった、関数コンポーネントの基本的な使い方を解説します。
4.1 Propsの受け取りと利用
関数コンポーネントは、引数として単一のオブジェクトを受け取ります。このオブジェクトには、親コンポーネントから渡されたすべてのPropsが格納されています。
“`javascript
// 親コンポーネント
function App() {
return (
);
}
// 子コンポーネント(関数コンポーネント)
function Greeting(props) {
// propsオブジェクトからnameとmessageプロパティにアクセス
return (
{props.message}
Hello, {props.name}!
);
}
“`
Propsオブジェクトから個々のプロパティにアクセスするには、props.propertyName
のようにドット記法を使います。より一般的には、JavaScriptの分割代入(Destructuring Assignment)を使ってPropsを取り出すことが多いです。
javascript
// 分割代入を使った例
function Greeting({ name, message }) {
return (
<div>
<h1>{message}</h1>
<p>Hello, {name}!</p>
</div>
);
}
分割代入を使うことで、コードがより簡潔になり、コンポーネントがどのようなPropsを受け取るのかが一目で分かりやすくなります。Propsには、文字列、数値、真偽値、配列、オブジェクト、さらには関数なども渡すことができます。
“`javascript
// 関数をPropsとして渡す例
function Parent() {
const handleClick = () => {
alert(‘Button clicked!’);
};
return (
);
}
function Child({ onButtonClick, buttonText }) {
return (
);
}
“`
4.2 JSXの返却規則
関数コンポーネントは、レンダリングするUIとしてJSXを返却します。ただし、いくつかの規則があります。
-
単一のルート要素: JSXを返す場合、通常は単一のルート要素(
<div>
,<p>
,<span>
など)で全体を囲む必要があります。“`javascript
// OK
function MyComponent() {
return (Title
Content
);
}// NG (隣接する要素)
/
function MyComponent() {
return (Title
Content
// Syntax Error
);
}
/
“` -
React.Fragment: 単一のルート要素で囲みたくない場合(例えば、CSSのレイアウトを壊したくない場合など)は、
React.Fragment
またはその短縮構文(<>
)を使用できます。FragmentはDOMには描画されません。“`javascript
import React from ‘react’; // React.Fragmentを使う場合はインポートが必要// React.Fragmentを使った例
function MyComponentWithFragment() {
return (
Title
Content
);
}// 短縮構文を使った例(より一般的)
function MyComponentWithFragmentShorthand() {
return (
<>Title
Content
);
}
“` -
null, boolean, undefinedの返却: コンポーネント何もレンダリングしない場合は、
null
,false
,true
,undefined
を返すことができます。これは条件付きレンダリングで便利です。数値の0
はレンダリングされることに注意してください。javascript
function OptionalContent({ shouldShow }) {
if (!shouldShow) {
return null; // 何も表示しない
}
return <p>This content is optional.</p>;
}
4.3 条件付きレンダリング
Propsやステートの値に基づいて、表示するUIを切り替えることを条件付きレンダリングと言います。関数コンポーネントでは、通常のJavaScriptの条件分岐(if
/else
)や論理演算子 (&&
, ? :
) を使って簡単に実現できます。
-
if
/else
ステートメント:javascript
function LoginButton({ isLoggedIn }) {
if (isLoggedIn) {
return <button>Logout</button>;
} else {
return <button>Login</button>;
}
} -
論理AND (
&&
) 演算子: 条件が真の場合に要素をレンダリングしたい場合に便利です。javascript
function Mailbox({ unreadMessages }) {
return (
<div>
<h1>Hello!</h1>
{/* unreadMessages > 0 が真の場合、<p>...</p> がレンダリングされる */}
{unreadMessages > 0 &&
<p>You have {unreadMessages} unread messages.</p>
}
</div>
);
}
注意点として、&&
の左辺が0
の場合、右辺の要素はレンダリングされず、代わりに0
が表示されてしまいます。したがって、条件式はブール値になるように工夫するか、後述の三項演算子を使うのがより安全です。 -
三項演算子 (
? :
):if
/else
よりも簡潔に条件に応じた要素を切り替える場合に便利です。javascript
function UserStatus({ user }) {
return (
<div>
{user ? (
<p>Welcome, {user.name}!</p>
) : (
<p>Please log in.</p>
)}
</div>
);
}
4.4 リストレンダリング
配列のデータを基に要素のリストをレンダリングする場合、JavaScriptの配列メソッドであるmap
を使用します。map
メソッドは配列の各要素に対してコールバック関数を実行し、その結果を新しい配列として返します。Reactはこの新しい配列に含まれるJSX要素をリストとしてレンダリングします。
“`javascript
function TodoList({ todos }) {
return (
-
{/ todos配列をmapして、各todoオブジェクトから
- 要素を生成 /}
- {todo.text}
{todos.map(todo => (
// ★重要★ リスト内の各要素には一意なkeyプロパティを付ける必要があります
))}
);
}
// 親コンポーネントでの利用例
function App() {
const myTodos = [
{ id: 1, text: ‘Learn React’ },
{ id: 2, text: ‘Build a project’ },
{ id: 3, text: ‘Deploy the app’ },
];
return (
);
}
“`
key
属性の重要性:
リストをレンダリングする際に、key
属性を付けることは非常に重要です。key
はReactがリスト内の各要素を識別するための特別な文字列または数値です。Reactはkey
を使って、リスト内の要素が追加、削除、並べ替えられたことを効率的に検出し、最小限のDOM操作でUIを更新します。
key
には、リスト内で一意な値(データベースのIDなど)を使用する必要があります。- リストの要素のインデックス(配列の
index
)をkey
として使うことも可能ですが、リストが静的で変更(追加、削除、並べ替え)されない場合に限定すべきです。リストが変更される可能性がある場合は、要素自身の安定した一意なIDをkey
として使用しないと、意図しない挙動やパフォーマンスの問題が発生する可能性があります。
4.5 イベントハンドリング
ボタンのクリックや入力フィールドの値変更など、ユーザーの操作に応じた処理は、イベントハンドラーを介して行います。Reactでは、DOM要素にイベントリスナーを直接追加する代わりに、JSXの属性としてイベントハンドラー関数を指定します。属性名はキャメルケースで記述します(例: onClick
, onChange
, onSubmit
)。
“`javascript
function MyButton() {
// イベントハンドラー関数を定義
const handleClick = () => {
alert(‘Button clicked!’);
};
return (
// onClick属性に関数ハンドラーを指定
);
}
“`
イベントハンドラー関数には、通常、ブラウザのネイティブイベントオブジェクトをラップした「合成イベントオブジェクト」(Synthetic Event)が引数として渡されます。この合成イベントオブジェクトは、ブラウザ間の互換性を確保するためにReactによって提供されます。
“`javascript
function MyInput() {
const handleChange = (event) => {
// 合成イベントオブジェクトから入力値を取得
console.log(‘Input value:’, event.target.value);
};
return (
);
}
“`
イベントハンドラーに関数を渡す際は、関数定義そのものを渡す必要があります (onClick={handleClick}
)。関数呼び出しの結果を渡してしまうと、コンポーネメントがレンダリングされた瞬間にハンドラーが実行されてしまいます (onClick={handleClick()}
は間違いです)。
イベントハンドラーに引数を渡したい場合は、アロー関数でラップします。
“`javascript
function MyList({ items }) {
// イベントハンドラー関数(引数を受け取る)
const handleItemClick = (item) => {
console.log(‘Clicked item:’, item);
};
return (
-
{items.map(item => (
-
{/ アロー関数でラップして、handleItemClickに関数を呼び出すようにする /}
))}
);
}
“`
5. Hooksによる関数コンポーネントの機能拡張
React Hooksは、関数コンポーネントにステートやライフサイクル、コンテキストといったReactの機能を「フック」するための仕組みです。Hooksの登場により、関数コンポーネントはクラスコンポーネントと同等以上の表現力を持つに至りました。
5.1 Hooksの概念とルール
Hooksは、コンポーネントのトップレベルでのみ呼び出すことができます。ループ、条件分岐、ネストされた関数の中からHooksを呼び出すことはできません。このルールを守ることで、ReactはレンダリングごとにどのHooksが呼び出されたかを正確に追跡できます。
Hooksには多くの種類がありますが、ここでは主要なHooksについて解説します。
useState
: 関数コンポーネントにローカルなステートを追加します。useEffect
: 関数コンポーネントで副作用(データの取得、DOM操作、購読など)を実行します。useContext
: Context APIからコンテキストの値を読み取ります。useRef
: レンダリング間で変化しない値を保持したり、DOM要素への参照を取得したりします。useReducer
:useState
よりも複雑なステート管理を行う場合に利用します。useCallback
: 関数のメモ化を行い、不要な関数再作成による子コンポーネントの再レンダリングを防ぎます。useMemo
: 計算コストが高い処理の結果をメモ化し、不要な再計算を防ぎます。
5.2 useState
による状態管理
useState
は、関数コンポーネントにローカルなステート(状態)を追加するためのフックです。useState
を呼び出すと、現在のステートの値と、そのステートを更新するための関数がペアで返されます。
基本構文:
“`javascript
import React, { useState } from ‘react’;
function Counter() {
// countというステート変数と、setCountという更新関数を宣言
// 初期値は0
const [count, setCount] = useState(0);
const increment = () => {
// setCount関数を呼び出してステートを更新
setCount(count + 1);
};
return (
Count: {count}
);
}
“`
useState(initialState)
:useState
を呼び出す際に、ステートの初期値を引数として渡します。初期値は初回レンダリング時のみ使用されます。[state, setState]
:useState
は配列を返します。配列の最初の要素は現在のステートの値、二番目の要素はそのステートを更新するための関数です(慣習的にset
+ ステート変数名 という名前が使われます)。配列の分割代入を使ってこれらの値を取り出します。
ステートの更新:
ステートを更新するには、setCount
のような更新関数を呼び出します。setCount(newValue)
のように新しいステートの値を直接渡すか、前のステートに基づいた更新の場合は関数を渡します。
javascript
// 前のステートに基づいた更新(推奨)
const increment = () => {
setCount(prevCount => prevCount + 1); // prevCountは更新前のステートの値
};
更新関数に関数を渡す形式(prevCount => prevCount + 1
)は、非同期的に行われる可能性のあるステート更新において、常に最新のステート値に基づいて計算を行うことを保証します。特に、連続して何度もステートを更新する場合や、非同期処理の中でステートを更新する場合に重要になります。
オブジェクトや配列の更新:
useState
はプリミティブ型だけでなく、オブジェクトや配列もステートとして保持できます。ただし、ステートがオブジェクトや配列の場合、既存のオブジェクトや配列を直接変更(ミューテーション)するのではなく、常に新しいオブジェクトや配列を生成して更新関数に渡す必要があります。 Reactはオブジェクトや配列の参照が変更されたかどうかを見て再レンダリングを判断するため、直接変更しても再レンダリングが起きないか、予期しない動作になる可能性があります。
“`javascript
import React, { useState } from ‘react’;
function UserProfile() {
const [user, setUser] = useState({ name: ‘Guest’, age: null });
const updateName = () => {
// 新しいオブジェクトを生成して更新
setUser({ …user, name: ‘Alice’ }); // スプレッド構文で既存のプロパティをコピーし、nameを上書き
};
const addHobby = (hobby) => {
// 新しい配列を生成して更新
setUser({
…user,
hobbies: user.hobbies ? […user.hobbies, hobby] : [hobby] // 既存の配列をコピーし、新しい要素を追加
});
};
return (
Name: {user.name}
{user.age &&
Age: {user.age}
}
{user.hobbies && (
-
Hobbies: {user.hobbies.map((h, i) =>
- {h}
)}
)}
);
}
“`
遅延初期化:
useState
の初期値が計算コストの高い処理の結果に依存する場合、初期値を直接渡す代わりに、初期値を計算する関数を渡すことができます。この関数は初回レンダリング時のみ実行されます。
“`javascript
const initialState = expensiveCalculation(props); // レンダリングごとに実行される
// 初期値を計算する関数を渡す(初回レンダリング時のみ実行)
const [state, setState] = useState(() => expensiveCalculation(props));
“`
これにより、コンポーネントが再レンダリングされるたびに不要な計算が実行されるのを防ぎ、パフォーマンスを向上させることができます。
複数のuseState
:
一つの関数コンポーネント内で、必要に応じて複数のuseState
を呼び出すことができます。それぞれのuseState
呼び出しは独立したステート変数とその更新関数を提供します。
“`javascript
import React, { useState } from ‘react’;
function MyForm() {
const [name, setName] = useState(”);
const [email, setEmail] = useState(”);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = (event) => {
event.preventDefault();
setIsSubmitting(true);
// フォーム送信ロジック…
console.log(‘Submitting:’, { name, email });
// 完了したらステートをリセットなど
setIsSubmitting(false);
};
return (
);
}
“`
このように、複数のステート変数を宣言することで、コンポーネントの状態を細かく管理できます。
5.3 useEffect
による副作用の処理
useEffect
は、関数コンポーネント内で副作用(side effects)を実行するためのフックです。副作用とは、Reactのレンダリングプロセス中に直接行うべきではない操作のことです。例えば、データフェッチ(API呼び出し)、DOMの直接操作、タイマーの設定、イベントリスナーの登録・解除、購読の設定・解除などが副作用にあたります。
クラスコンポーネントでは、これらの副作用は主にcomponentDidMount
, componentDidUpdate
, componentWillUnmount
といったライフサイクルメソッドで行われていました。useEffect
フックは、これらのライフサイクルメソッドの役割を一つにまとめたようなものです。
基本構文:
“`javascript
import React, { useState, useEffect } from ‘react’;
function Timer() {
const [seconds, setSeconds] = useState(0);
// 副作用を定義
useEffect(() => {
// この関数が副作用です
const intervalId = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
// クリーンアップ関数を返す
return () => {
// この関数はコンポーネントがアンマウントされる時や、
// 依存配列が変更されてエフェクトが再実行される直前に実行されます
clearInterval(intervalId);
};
}, []); // ★重要★ 依存配列
// []が指定されているため、このエフェクトはマウント時のみ実行され、
// アンマウント時にクリーンアップされます。
return (
Seconds: {seconds}
);
}
“`
useEffect
は第一引数に副作用を実行する関数を、第二引数に「依存配列」(Dependencies Array)を受け取ります。
実行タイミングと依存配列:
useEffect
で指定した副作用関数は、デフォルトではコンポーネントの初回レンダリング後およびそれ以降のすべての再レンダリング後に実行されます。しかし、第二引数の依存配列によって、この実行タイミングを制御できます。
-
依存配列を省略:
useEffect(() => { ... });
→ 副作用はコンポーネントの初回レンダリング後と、その後のすべての再レンダリング後に実行されます。これは、ステートやPropsの変更によってコンポーネントが更新されるたびに、副作用も常に最新の状態を反映するように実行したい場合に便利です。ただし、頻繁に実行されるとパフォーマンス問題を引き起こす可能性があります。 -
空の依存配列
[]
:
useEffect(() => { ... }, []);
→ 副作用はコンポーネントの初回レンダリング後のみ実行されます。その後の再レンダリングでは実行されません。これは、componentDidMount
に相当する挙動です。外部APIからのデータ取得や、一度だけ設定すれば良い購読などに使用します。空配列を渡すと、エフェクトはコンポーネントのライフサイクルを通じて一度だけ実行されることをReactに伝えます。 -
依存配列に値を指定
[value1, value2, ...]
:
useEffect(() => { ... }, [someProp, someState]);
→ 副作用はコンポーネントの初回レンダリング後と、依存配列に含まれるいずれかの値が前回のレンダリングから変更された場合にのみ実行されます。これは、componentDidMount
とcomponentDidUpdate
の一部に相当する挙動です。エフェクト内で使用しているPropsやステートなど、変更を監視したい値を指定します。エフェクト内で使用しているコンポーネントスコープの値(Props, ステート, コンポーネント内で定義された関数など)は、依存配列に含めるべきです。 ESLintのreact-hooks/exhaustive-deps
ルールは、このルールを守るのを助けてくれます。
クリーンアップ関数:
useEffect
の副作用関数は、必要に応じてクリーンアップ関数を返すことができます。クリーンアップ関数は、コンポーネントがアンマウントされる時(componentWillUnmount
に相当)や、依存配列が変更されてエフェクトが再実行される直前に実行されます。これは、エフェクトによって設定された購読の解除、タイマーのクリア、イベントリスナーの削除など、メモリリークや不要な処理を防ぐために重要です。
“`javascript
useEffect(() => {
const subscription = someApi.subscribe(); // 購読を開始
return () => {
subscription.unsubscribe(); // クリーンアップで購読を解除
};
}, []); // この購読はマウント時のみ開始され、アンマウント時に解除される
“`
依存配列が空でない場合、エフェクトが再実行される前にもクリーンアップ関数が実行されます。これは、新しい依存値に基づいて新しい副作用が実行される前に、古い副作用によって設定されたものをクリーンアップするためです。
“`javascript
useEffect(() => {
console.log(‘Fetching data for user ID:’, userId);
const controller = new AbortController(); // API呼び出しの中止に使用
fetch(/api/users/${userId}
, { signal: controller.signal })
.then(response => response.json())
.then(data => console.log(‘User data:’, data))
.catch(err => {
if (err.name === ‘AbortError’) {
console.log(‘Fetch aborted’);
} else {
console.error(‘Fetch error:’, err);
}
});
return () => {
console.log(‘Cleaning up effect for user ID:’, userId);
controller.abort(); // エフェクト再実行時やアンマウント時にAPI呼び出しを中止
};
}, [userId]); // userIdが変更されたらエフェクトを再実行
``
userId
この例では、が変更されるたびに前回の
fetchが中断され、新しい
fetch`が開始されます。これにより、古いAPIリクエストの結果が遅れて到着し、意図しないステート更新を引き起こすのを防ぐことができます。
複数のuseEffect
:
一つのコンポーネント内で、目的の異なる複数のuseEffect
を定義することができます。例えば、データ取得用のエフェクト、DOM操作用のエフェクト、購読用のエフェクトなどを分離して記述することで、関連するロジックをまとめて管理しやすくなります。
“`javascript
import React, { useState, useEffect } from ‘react’;
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// データ取得のエフェクト
useEffect(() => {
setLoading(true);
setError(null);
const controller = new AbortController();
const signal = controller.signal;
fetch(`/api/users/${userId}`, { signal })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') {
setError(err);
setLoading(false);
}
});
return () => {
controller.abort();
};
}, [userId]); // userIdが変更されたら再実行
// タイトル設定のエフェクト(DOM操作)
useEffect(() => {
if (user) {
document.title = Profile: ${user.name}
;
} else {
document.title = ‘Loading profile…’;
}
// クリーンアップ関数は不要(上書きされるだけのため)
}, [user]); // userが変更されたら再実行
if (loading) return
Loading…
;
if (error) return
Error: {error.message}
;
if (!user) return
No user data available.
;
return (
{user.name}
Email: {user.email}
{/ 他のユーザー情報 /}
);
}
``
useEffect`を分割することで、コードの可読性と保守性が向上します。
このように、役割ごとに
5.4 useContext
によるコンテキストの使用
Context APIは、Propsバケツリレー(深くネストされたコンポーネントツリーを通じてPropsを手渡ししていくこと)を避けるために、コンポーネントツリー全体にわたってデータを共有する仕組みです。useContext
フックは、関数コンポーネントからこのContextの値に簡単にアクセスするためのフックです。
基本構文:
まず、React.createContext
でContextオブジェクトを作成します。
“`javascript
// src/contexts/ThemeContext.js
import React from ‘react’;
// Contextオブジェクトを作成(デフォルト値は’light’)
export const ThemeContext = React.createContext(‘light’);
“`
次に、Contextオブジェクトの.Provider
コンポーネントを使って、コンポーネントツリーの上位で共有したい値を指定します。
“`javascript
// src/App.js
import React, { useState } from ‘react’;
import { ThemeContext } from ‘./contexts/ThemeContext’;
import ThemedComponent from ‘./components/ThemedComponent’;
import ThemeSwitcher from ‘./components/ThemeSwitcher’;
function App() {
const [theme, setTheme] = useState(‘light’);
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === ‘light’ ? ‘dark’ : ‘light’);
};
return (
// Context ProviderでthemeとtoggleTheme関数を共有
My App
);
}
“`
そして、子孫コンポーネントではuseContext
フックを使ってContextの値にアクセスします。
“`javascript
// src/components/ThemedComponent.js
import React, { useContext } from ‘react’;
import { ThemeContext } from ‘../contexts/ThemeContext’;
function ThemedComponent() {
// useContextフックを使ってContextの値({ theme, toggleTheme })を取得
const { theme } = useContext(ThemeContext);
// コンテキストの値に応じてスタイルを適用
const style = {
backgroundColor: theme === ‘light’ ? ‘#fff’ : ‘#333’,
color: theme === ‘light’ ? ‘#333’ : ‘#fff’,
padding: ’20px’,
marginTop: ’20px’
};
return (
This component’s theme is “{theme}”.
);
}
// src/components/ThemeSwitcher.js
import React, { useContext } from ‘react’;
import { ThemeContext } from ‘../contexts/ThemeContext’;
function ThemeSwitcher() {
// useContextフックを使ってContextの値({ theme, toggleTheme })を取得
const { theme, toggleTheme } = useContext(ThemeContext);
return (
);
}
``
useContext(ContextObject)`と呼び出すだけで、最も近い上位のProviderから提供されるContextの値を取得できます。これにより、Propsを中間コンポーネントで手渡しする手間が省け、コードがスッキリします。
Contextはステート管理ツールとしてReduxやZustandなどのライブラリと比較されることがありますが、Contextはあくまで「データの受け渡し」に特化した機能です。シンプルな全体的なステート管理には十分ですが、アプリケーションの規模が大きくなったり、複雑なステート遷移が必要になったりする場合は、専用のステート管理ライブラリの導入を検討すると良いでしょう。
5.5 useRef
による永続的な値とDOM参照
useRef
フックは、レンダリング間で保持される変更可能な値を持つためのフックです。useState
と似ていますが、useRef
で管理される値は変更されても再レンダリングをトリガーしません。主に以下の二つの目的で使用されます。
- DOM要素への参照: 特定のDOM要素に直接アクセスしたい場合に使用します。
- レンダリング間で変化しない可変値の保持: タイマーID、前回のProps/ステートの値、購読オブジェクトなど、コンポーネントの生存期間中に維持したいが、変更されても再レンダリングは不要な値を保持する場合に使用します。
基本構文:
“`javascript
import React, { useRef, useEffect } from ‘react’;
function MyComponentWithRef() {
// 初期値nullでrefオブジェクトを作成
const myInputRef = useRef(null);
const timerIdRef = useRef(null);
const previousValueRef = useRef(null);
// DOM要素への参照例
useEffect(() => {
// currentプロパティを通じてDOM要素にアクセス
if (myInputRef.current) {
myInputRef.current.focus(); // インプットフィールドにフォーカス
}
}, []); // マウント時に一度だけ実行
// レンダリング間で可変値を保持する例
useEffect(() => {
timerIdRef.current = setInterval(() => {
console.log(‘Timer tick’);
}, 1000);
// クリーンアップでタイマーをクリア
return () => {
clearInterval(timerIdRef.current);
};
}, []); // マウント/アンマウント時に実行
// 前回の値を保持する例
const currentValue = ‘some value’; // 仮の値
useEffect(() => {
// エフェクトが実行される時点での現在の値を、次回のレンダリングのために保存
previousValueRef.current = currentValue;
}, [currentValue]); // currentValueが変更されるたびに実行
console.log(‘Previous value:’, previousValueRef.current); // レンダリング時に表示
return (
Previous value: {previousValueRef.current}
);
}
“`
useRef(initialValue)
:useRef
を呼び出す際に、.current
プロパティの初期値を引数として渡します。refObject.current
:useRef
は、current
というプロパティを持つプレーンなJavaScriptオブジェクトを返します。この.current
プロパティの値は変更可能であり、その変更は再レンダリングをトリガーしません。コンポーネントのライフサイクル全体を通じて、このref
オブジェクトのインスタンスは同じものが保持されます。
useRef
とuseState
の違い:
特徴 | useState |
useRef |
---|---|---|
値の変更 | setState 関数を使用 |
.current プロパティを直接変更 |
再レンダリング | 値が変更されるとコンポーネントが再レンダリングされる | 値が変更されてもコンポーネントは再レンダリングされない |
目的 | UIに表示したり、再レンダリングのトリガーとしたい「状態」を管理する | レンダリング間で値を保持したいが、変更がUIに影響しない「永続的な値」を保持する。DOM参照。 |
初期化 | 初回レンダリング時のみ実行される初期化関数を渡せる | 初回レンダリング時のみ初期値が設定される |
UIに表示される値や、その変更によってUIを更新したい場合はuseState
を、そうでない内部的な値やDOM参照にはuseRef
を使用するのが適切です。
5.6 useReducer
による複雑な状態管理
useReducer
は、useState
の代替となるフックで、特にステートのロジックが複雑な場合や、複数のステート変数が関連している場合に有用です。Reduxのようなreducerパターンに基づいています。
基本構文:
“`javascript
import React, { useReducer } from ‘react’;
// 1. Reducer関数を定義する
// (currentState, action) => newState
function counterReducer(state, action) {
switch (action.type) {
case ‘increment’:
return { count: state.count + 1 };
case ‘decrement’:
return { count: state.count – 1 };
case ‘reset’:
return { count: action.payload }; // actionにデータを含めることも可能
default:
return state; // 未知のアクションは現在のステートを返す
}
}
// 2. useReducerフックを使用する
function CounterWithReducer() {
// [現在のstate, dispatch関数] = useReducer(reducer関数, 初期state);
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
Count: {state.count}
{/ dispatch関数にactionオブジェクトを渡してstateを更新 /}
);
}
“`
useReducer(reducer, initialState, init?)
:useReducer
フックは3つの引数を取ることができます。reducer
: 現在のステートとアクションを受け取り、新しいステートを返す関数です。initialState
: ステートの初期値です。init
(省略可能): 遅延初期化のための関数です。init(initialState)
の結果が最初のステートとなります。
[state, dispatch]
:useReducer
は配列を返します。最初の要素は現在のステートの値、二番目の要素はステートを更新するためのdispatch
関数です。
dispatch
関数とaction
オブジェクト:
ステートを更新するには、dispatch
関数を呼び出し、ステートの変更内容を表す「アクション」オブジェクトを引数として渡します。アクションオブジェクトは通常、type
プロパティを持ち、必要に応じて変更に関連するデータ(payload
など)を含みます。dispatch(action)
が呼び出されると、Reactは現在のステートと渡されたアクションをreducer関数に渡し、reducer関数が返した新しいステートでコンポーネントを再レンダリングします。
useState
とuseReducer
の使い分け:
useState
: ステートの更新ロジックがシンプルで、単一のステート変数や関連性の薄い複数のステート変数がある場合に適しています。更新が直接的で分かりやすいです。useReducer
: ステートの更新ロジックが複雑で、多くの異なるアクションタイプがある場合、または複数のステート変数が密接に関連しており、それらの更新が同期的に行われる必要がある場合に適しています。関連する更新ロジックをreducer関数として一箇所にまとめられるため、コードが整理されます。また、Context APIと組み合わせてグローバルなステート管理の簡易版として使用されることもあります。
複雑なフォームのステート管理、状態遷移が多いUI要素(例: ローディング状態、エラー状態などを含むデータフェッチ)、ローカルなショッピングカート管理などにuseReducer
は力を発揮します。
5.7 useCallback
による関数のメモ化
useCallback
フックは、関数の定義自体をメモ化(キャッシュ)するためのフックです。特定の関数がPropsとして子コンポーネントに渡される場合に、不要な関数の再作成を防ぎ、それに起因する子コンポーネントの不要な再レンダリングを抑制するために使用されます。
問題点: 関数コンポーネントが再レンダリングされるたびに、そのコンポーネント内で定義された関数は毎回新しく作成されます。親コンポーネントから子コンポーネントに関数をPropsとして渡している場合、親が再レンダリングされると、新しい関数が作成され、子に渡されます。子がPropsの変更をチェックする際に、新しい関数は前回の関数とは参照が異なるため、「Propsが変更された」と判断し、子コンポーネリングも再レンダリングされてしまいます。これは、子コンポーネントがReact.memo
などで最適化されている場合に特に問題となります。
useCallback
の使い方:
“`javascript
import React, { useState, useCallback } from ‘react’;
import Button from ‘./Button’; // React.memoでメモ化された子コンポーネント
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState(”);
// countが変わったときだけこの関数を再作成
const handleIncrement = useCallback(() => {
setCount(count + 1);
}, [count]); // 依存配列にcountを指定
// textが変わったときだけこの関数を再作成
const handleChange = useCallback((e) => {
setText(e.target.value);
}, [text]); // 依存配列にtextを指定
// 依存配列が空の場合、この関数は初回レンダリング時のみ作成され、以降再作成されない
const handleClickWithoutDeps = useCallback(() => {
console.log(‘Button clicked’);
}, []); // 依存配列が空
return (
Count: {count}
{/ 関数自体を渡す /}
<input type="text" value={text} onChange={handleChange} /> {/* 関数自体を渡す */}
{/* メモ化された子コンポーネントに安定した関数を渡す */}
<Button onClick={handleClickWithoutDeps}>Static Button</Button>
</div>
);
}
// ./Button.js (例:React.memoでメモ化された子コンポーネント)
import React from ‘react’;
const Button = React.memo(({ onClick, children }) => {
console.log(‘Button rendered!’); // ボタンがレンダリングされるたびにログが出る
return ;
});
export default Button;
“`
useCallback(callbackFunction, dependencies)
:callbackFunction
: メモ化したい関数です。dependencies
: 関数の定義が依存する値(Propsやステートなど)の配列です。依存配列に含まれるいずれかの値が変更された場合にのみ、新しい関数が作成されます。依存配列が空 ([]
) の場合、関数は初回レンダリング時のみ作成され、以降は同じ関数オブジェクトが再利用されます。
useCallback
は、主に以下のケースで役立ちます。
React.memo
でメモ化された子コンポーネントにコールバック関数をPropsとして渡す場合。useEffect
やuseMemo
の依存配列に含める関数を定義する場合(関数自身が変更されるとエフェクトやメモ化が再実行されてしまうため、安定した関数参照が必要な場合)。
注意点として、useCallback
自体にもオーバーヘッドがあるため、すべての関数をuseCallback
で囲む必要はありません。不要な再レンダリングが実際にパフォーマンスボトルネックになっている場合に限定して使用するのが推奨されます。特に、子コンポーネントがメモ化されていない場合は、親コンポーネントの再レンダリングによって常に再レンダリングされるため、関数をメモ化するメリットはほとんどありません。
5.8 useMemo
による値のメモ化
useMemo
フックは、計算コストの高い処理の結果をメモ化するためのフックです。依存配列に含まれる値が変更されない限り、メモ化された値が再利用され、不要な再計算を防ぎます。
問題点: コンポーネントが再レンダリングされるたびに、そのコンポーネント内で定義されたJavaScriptコードはすべて実行されます。もしレンダリング中に計算コストが高い処理(例: 大量のデータのフィルタリングやソート、複雑な計算)を実行している場合、不要な再計算が頻繁に行われ、パフォーマンスが低下する可能性があります。
useMemo
の使い方:
“`javascript
import React, { useState, useMemo } from ‘react’;
function ShoppingCart({ items }) {
const [taxRate, setTaxRate] = useState(0.08);
// items または taxRate が変更された場合にのみ合計金額を再計算
const total = useMemo(() => {
console.log(‘Calculating total…’); // 再計算された場合にログが出る
return items.reduce((sum, item) => sum + item.price * item.quantity, 0) * (1 + taxRate);
}, [items, taxRate]); // 依存配列に items と taxRate を指定
const handleAddItem = () => {
// items を更新するロジック(省略)
};
return (
Shopping Cart
-
{items.map(item => (
- {item.name} – ${item.price} x {item.quantity}
))}
Subtotal: ${items.reduce((sum, item) => sum + item.price * item.quantity, 0).toFixed(2)}
Tax Rate: {taxRate * 100}%
Total: ${total.toFixed(2)}
{/ メモ化された値を表示 /}
{/* taxRate に依存しない他のステートや Props が変更されても total は再計算されない */}
<button onClick={() => setTaxRate(0.10)}>Set Tax Rate to 10%</button>
{/* <button onClick={handleAddItem}>Add Item</button> */} {/* このボタンは total の再計算をトリガーする */}
</div>
);
}
“`
useMemo(factoryFunction, dependencies)
:factoryFunction
: メモ化したい値を計算する関数です。この関数は引数を取らず、計算結果を返します。dependencies
: 計算が依存する値(Propsやステートなど)の配列です。依存配列に含まれるいずれかの値が変更された場合にのみ、factoryFunction
が再実行され、新しい値がメモ化されます。依存配列が空 ([]
) の場合、関数は初回レンダリング時のみ実行され、以降は常に同じ値が再利用されます。
useMemo
は、主に以下のケースで役立ちます。
- 計算コストが高い処理の結果をキャッシュしたい場合。
- 子コンポーネントにPropsとして渡すオブジェクトや配列を生成する場合。オブジェクトや配列は参照型であるため、親が再レンダリングされるたびに新しいオブジェクト/配列を生成してしまうと、
React.memo
で最適化された子コンポーネントでもPropsの変更と見なされて不要な再レンダリングが発生してしまいます。useMemo
でオブジェクト/配列の生成をメモ化することで、依存値が変わらない限り同じオブジェクト/配列参照を子に渡すことができ、子の再レンダリングを防ぐことができます。
useCallback
は関数をメモ化し、useMemo
は計算結果の「値」をメモ化します。どちらもパフォーマンス最適化のために使用されますが、メモ化の対象が異なります。useMemo
もまた、すべての計算に適用する必要はなく、実際にパフォーマンスボトルネックとなっている箇所に限定して使用するのが一般的です。
5.9 カスタムHooks
Hooksの強力な特徴の一つに、独自のカスタムHooksを作成できるという点があります。カスタムHooksを使うことで、コンポーネント間でステートフルなロジックを簡単に再利用できます。
カスタムHooksを作る理由:
- ロジックの再利用: 複数のコンポーネントで同じロジック(例: データ取得、フォーム入力処理、ウィンドウサイズ取得)が必要な場合に、カスタムHooksとして切り出すことで重複をなくし、コードをDRY(Don’t Repeat Yourself)に保てます。
- 関心の分離: コンポーネントから非UIのロジックを切り離すことで、コンポーネントの役割をUIの表示に集中させ、コードをよりシンプルで読みやすくします。
- テストのしやすさ: UIから分離されたロジックは、単体テストが容易になります。
カスタムHooksのルール:
カスタムHooksは以下のルールに従う必要があります。
- 命名規則: カスタムHooksの名前は必ず
use
というプレフィックスで始める必要があります(例:useFetch
,useFormInput
,useWindowSize
)。これにより、ReactはそれがHooksのルールに従うHooksであることを認識できます。 - Hooksの内部呼び出し: カスタムHooksの内部では、他のHooks(
useState
,useEffect
,useContext
など)や他のカスタムHooksを呼び出すことができます。
カスタムHooksの作成例:
例えば、APIからデータを取得するロジックを複数のコンポーネントで使いたいとします。
“`javascript
// src/hooks/useFetch.js
import { useState, useEffect } from ‘react’;
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
setLoading(true);
setError(null);
fetch(url, { signal })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') {
setError(err);
setLoading(false);
}
});
return () => {
controller.abort();
};
}, [url]); // urlが変更されたら再実行
return { data, loading, error }; // 取得したデータ、ローディング状態、エラー情報を返す
}
export default useFetch;
“`
このカスタムHooksは、URLを受け取り、データ、ローディング状態、エラー情報を返します。コンポーネントはこのHooksを使うだけで、データ取得のロジックを自分で記述する必要がありません。
カスタムHooksの利用例:
“`javascript
// src/components/UserList.js
import React from ‘react’;
import useFetch from ‘../hooks/useFetch’; // カスタムHooksをインポート
function UserList() {
// カスタムHooksを呼び出してデータと状態を取得
const { data: users, loading, error } = useFetch(‘https://jsonplaceholder.typicode.com/users’);
if (loading) return
Loading users…
;
if (error) return
Error: {error.message}
;
if (!users) return
No users found.
;
return (
Users
-
{users.map(user => (
- {user.name} ({user.email})
))}
);
}
// src/components/PostList.js
import React from ‘react’;
import useFetch from ‘../hooks/useFetch’; // 同じカスタムHooksを別のコンポーネントでも利用
function PostList() {
const { data: posts, loading, error } = useFetch(‘https://jsonplaceholder.typicode.com/posts’);
if (loading) return
Loading posts…
;
if (error) return
Error: {error.message}
;
if (!posts) return
No posts found.
;
return (
Posts
-
{posts.map(post => (
- {post.title}
))}
);
}
``
useFetch
このように、同じカスタムHooksを
UserListコンポーネントと
PostListコンポーネントで再利用できます。ロジックは
useFetch.js`ファイルにカプセル化されており、各コンポーネントはUIの表示に集中できます。
カスタムHooksは、関数コンポーネントにおけるロジックの抽象化と再利用のための非常に強力なパターンです。
6. パフォーマンス最適化
関数コンポーネントとHooksを使った開発において、パフォーマンスは重要な考慮事項です。特に大規模なアプリケーションでは、不要な再レンダリングを最小限に抑えることがUIの応答性を保つために不可欠です。Hooks (useCallback
, useMemo
) や React.memo
を使うことで、パフォーマンスを向上させることができます。
6.1 React.memo
を使ったコンポーネントのメモ化
React.memo
は高階コンポーネント(Higher-Order Component – HOC)で、関数コンポーネントをラップして、そのPropsが変更された場合にのみ再レンダリングするように最適化します。Propsが変更されていない場合は、前回のレンダリング結果を再利用します。
“`javascript
import React from ‘react’;
// 通常の関数コンポーネント
const MyComponent = ({ name, value }) => {
console.log(‘MyComponent rendered!’); // 再レンダリングされるたびにログが出る
return (
Name: {name}
Value: {value}
);
};
// React.memoでメモ化
const MemoizedMyComponent = React.memo(MyComponent);
// 親コンポーネント
function Parent() {
const [count, setCount] = React.useState(0);
const [data, setData] = React.useState({ name: ‘Test’, value: 123 });
return (
{/ count が変更されても、MemoizedMyComponent の props (data.name, data.value) は変わらないため、再レンダリングされない /}
{/* もし MemoizedMyComponent に渡すPropsにオブジェクトや関数が含まれる場合、
それらを useCallback や useMemo でメモ化しないと、参照が変わるため再レンダリングされてしまう
例: <MemoizedChild data={{ name: 'Test', value: 123 }} /> => 毎回新しいオブジェクトが作成される
*/}
</div>
);
}
“`
React.memo
は、デフォルトではPropsのシャロー比較(shallow comparison)を行います。つまり、Propsの値がプリミティブ型であれば値自体を、オブジェクトや配列などの参照型であれば参照が同じかどうかを比較します。参照型のPropsの中身が変わっていても参照が変わっていなければ再レンダリングは起きませんし、中身が変わっていなくても新しいオブジェクト/配列が生成されて参照が変わってしまうと再レンダリングが起きてしまいます。
オブジェクトや配列、関数をPropsとして渡す場合は、useMemo
やuseCallback
を使ってこれらの参照を安定させる必要があります。
React.memo
は、再レンダリングコストが高いコンポーネントや、Propsが頻繁に変更されないが親コンポーネントが頻繁に再レンダリングされる場合に効果的です。ただし、Propsの比較自体にもコストがかかるため、すべてのコンポーネントに適用する必要はありません。
6.2 useCallback
と useMemo
を React.memo
と組み合わせて使う
前述の通り、React.memo
でメモ化した子コンポーネントにオブジェクト、配列、関数をPropsとして渡す場合、親コンポーネントの再レンダリングによってこれらの参照が変わると、子が不要に再レンダリングされてしまいます。これを防ぐために、親コンポーネント側でuseMemo
やuseCallback
を使ってこれらのPropsをメモ化し、参照を安定させます。
“`javascript
import React, { useState, useMemo, useCallback } from ‘react’;
import MemoizedChildComponent from ‘./MemoizedChildComponent’; // React.memoでラップされた子コンポーネント
function ParentComponent() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([{ id: 1, name: ‘Item A’ }]);
// 関数をメモ化:countが変わったときだけ再作成
const handleIncrement = useCallback(() => {
setCount(count + 1);
}, [count]);
// オブジェクトをメモ化:itemsが変わったときだけ再作成
const memoizedData = useMemo(() => {
return {
list: items,
count: items.length
};
}, [items]);
// 配列をメモ化:itemsが変わったときだけ再作成
const memoizedItemsArray = useMemo(() => items.map(item => item.name), [items]);
return (
Parent Count: {count}
{/* 子コンポーネントにメモ化されたPropsを渡す */}
<MemoizedChildComponent
onButtonClick={handleIncrement} // メモ化された関数
data={memoizedData} // メモ化されたオブジェクト
itemNames={memoizedItemsArray} // メモ化された配列
/>
{/* 親コンポーネントの count だけが変わっても、
子の props (onButtonClick, data, itemNames) の参照は変わらないため、
MemoizedChildComponent は再レンダリングされない(items が変更された場合は再レンダリングされる)
*/}
</div>
);
}
“`
この組み合わせは、パフォーマンスがボトルネックになっている特定の箇所で効果を発揮します。過剰な最適化はコードを複雑にするだけなので、プロファイリングツールを使って本当に必要な箇所に適用することが重要です。
7. クラスコンポーネントからの移行
Hooksの登場により、関数コンポーネントはクラスコンポーネントと同等の機能を持つようになりました。現代のReact開発では関数コンポーネントが推奨されており、既存のクラスコンポーネントを関数コンポーネントに移行するケースも増えています。
移行のメリット:
- コードの簡潔さ: クラス構文のボイラープレートコード(
constructor
,super
,this
バインディングなど)が不要になり、コード量が減り、読みやすくなります。 - ロジックの再利用性: カスタムHooksを使うことで、ステートフルなロジックをコンポーネント間で簡単に共有・再利用できます。これはクラスコンポーネントでは難しかった点です(HOCやRender Propsパターンで実現は可能でしたが、より複雑になる傾向がありました)。
- 関連ロジックのグルーピング: クラスコンポーネントでは関連するロジック(例: データ取得と購読の解除)が異なるライフサイクルメソッドに分散しがちでしたが、
useEffect
を使うことで関連ロジックを一つにまとめることができます。 - 学習コスト: クラスの概念や
this
の挙動、多くのライフサイクルメソッドを理解する必要がなくなり、純粋なJavaScript関数とHooksという比較的にシンプルな概念に集中できます。
主要なライフサイクルメソッドとHooksの対応:
クラスコンポーネントの主要なライフサイクルメソッドは、主にuseEffect
とuseState
を使って関数コンポーネントで同等の処理を記述できます。
クラスコンポーネント | 関数コンポーネント (Hooks) | 説明 |
---|---|---|
constructor |
useState の初期値、useRef の初期値 |
初期化 |
componentDidMount |
useEffect(() => { ... }, []) |
コンポーネントのマウント後、一度だけ実行される処理(データ取得、購読開始など) |
componentDidUpdate(prevProps, prevState) |
useEffect(() => { ... }, [props, state]) |
PropsやStateの変更後に実行される処理 |
componentWillUnmount |
useEffect(() => { ...; return () => { ... } }, []) のクリーンアップ関数 |
コンポーネントがアンマウントされる直前に実行される処理(購読解除、タイマークリアなど) |
shouldComponentUpdate(nextProps, nextState) |
React.memo (Propsのシャロー比較) + useMemo /useCallback |
PropsやStateの変更時に再レンダリングが必要か判断 |
getDerivedStateFromProps |
Propsからステートを計算するロジックをレンダリングロジック内で記述 | Propsに基づいてステートを更新 |
移行のステップ:
- クラスを関数に変換:
React.Component
を継承したクラスを、Propsを受け取る関数に書き換えます。 this.state
をuseState
に変換: クラスのthis.state
に含まれる各ステート変数を、個別のuseState
呼び出しに変換します。this.setState
呼び出しは、対応するuseState
の更新関数(set...
関数)に置き換えます。ステートがオブジェクトの場合、非破壊的な更新(スプレッド構文など)を行うように変更します。- ライフサイクルメソッドを
useEffect
に変換:componentDidMount
で行っていた処理は、空の依存配列を持つuseEffect
(useEffect(() => { ... }, [])
) に移動します。購読解除やタイマークリアなどが必要な場合は、クリーンアップ関数を追加します。componentDidUpdate
で行っていた処理は、監視したいPropsやステートを依存配列に含むuseEffect
(useEffect(() => { ... }, [deps])
) に移動します。PropsやStateの変更前後の値が必要な場合は、useRef
を使って前回の値を保持するなどの工夫が必要になることがあります。componentWillUnmount
で行っていたクリーンアップ処理は、useEffect
のクリーンアップ関数 (return () => { ... }
) に移動します。
this
の参照を削除: メソッドのthis
バインディングは不要になります。Propsやステートの値、あるいは他のHooksによって提供される値(例:useContext
の値)を直接参照します。イベントハンドラー関数も、コンポーネント内で通常のアロー関数として定義します。- Refs を
useRef
に変換: DOM要素への参照や、レンダリング間で保持したい可変値はuseRef
に変換します。 - パフォーマンス最適化: 必要に応じて
React.memo
,useCallback
,useMemo
を適用します。 - テスト: 変換後のコンポーネントが正しく動作するか確認します。特に、
useEffect
の依存配列が正しく設定されているか、クリーンアップ処理が正しく行われているかなどを注意深くテストします。
すべてのクラスコンポーネントを関数コンポーネントに移行する必要はありませんが、新規開発や既存コードのリファクタリングの際には、関数コンポーネントとHooksを採用することが推奨されています。
8. 実践的な関数コンポーネントの例
これまでに解説したHooksを使って、いくつか実践的な関数コンポーネントの例を見てみましょう。
例1: シンプルなカウンター (useState
)
これは既に紹介しましたが、最も基本的なuseState
の例です。
“`javascript
import React, { useState } from ‘react’;
function Counter() {
const [count, setCount] = useState(0);
return (
Count: {count}
);
}
“`
例2: テキスト入力と表示 (useState
)
フォーム要素の値をステートで管理する基本的な例です。
“`javascript
import React, { useState } from ‘react’;
function InputDisplay() {
const [inputText, setInputText] = useState(”);
const handleInputChange = (event) => {
// event.target.value で入力値を取得し、ステートを更新
setInputText(event.target.value);
};
return (
Input Example
Current Input: {inputText}
);
}
``
value
入力フィールドの属性にステート変数を、
onChange`イベントにステート更新関数を指定することで、「制御されたコンポーネント」(Controlled Component)として実装するのがReactにおけるフォームの一般的なパターンです。
例3: APIからのデータ取得と表示 (useState
, useEffect
)
useEffect
を使って、コンポーネントのマウント時にAPIからデータを取得し、useState
でそのデータとローディング状態、エラー状態を管理する例です。
“`javascript
import React, { useState, useEffect } from ‘react’;
function PostDetail({ postId }) {
const [post, setPost] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
setLoading(true); // データ取得開始時にローディング状態をtrueに
setError(null); // エラー状態をリセット
fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, { signal })
.then(response => {
if (!response.ok) {
// HTTPエラーレスポンスの場合
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
setPost(data); // データをステートに保存
setLoading(false); // ローディング完了
})
.catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
setError(err); // エラーをステートに保存
setLoading(false); // ローディング完了(エラーのため)
}
});
// クリーンアップ関数: コンポーネントがアンマウントされる時やpostIdが変わる前に実行
return () => {
controller.abort(); // API呼び出しを中断
};
}, [postId]); // postIdが変更されたらエフェクトを再実行
if (loading) return
Loading post {postId}…
;
if (error) return
Error loading post {postId}: {error.message}
;
if (!post) return
No post found for ID {postId}.
; // データがまだない場合など
return (
{post.title}
{post.body}
{/ 他の投稿情報 /}
);
}
``
useEffect
この例では、の依存配列に
postIdを含めているため、親コンポーネントから渡される
postIdPropsが変更されるたびに新しい投稿データを再取得します。また、エフェクトのクリーンアップ関数で
AbortController`を使ってAPI呼び出しを中断することで、コンポーネントがアンマウントされた後にステート更新を行おうとして発生する可能性のあるエラー(メモリリーク警告など)を防いでいます。
例4: テーマ切り替え (useState
, useContext
)
Context APIとuseContext
を使って、アプリケーションのテーマを複数のコンポーネントで共有し、切り替える例です。
“`javascript
// src/contexts/ThemeContext.js (既に例示済み)
// import React from ‘react’;
// export const ThemeContext = React.createContext(‘light’);
// src/App.js (既に例示済み)
/*
import React, { useState } from ‘react’;
import { ThemeContext } from ‘./contexts/ThemeContext’;
import ThemedComponent from ‘./components/ThemedComponent’;
import ThemeSwitcher from ‘./components/ThemeSwitcher’;
function App() {
const [theme, setTheme] = useState(‘light’);
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === ‘light’ ? ‘dark’ : ‘light’);
};
return (
My App
);
}
*/
// src/components/ThemedComponent.js (既に例示済み)
/*
import React, { useContext } from ‘react’;
import { ThemeContext } from ‘../contexts/ThemeContext’;
function ThemedComponent() {
const { theme } = useContext(ThemeContext);
const style = { … }; // スタイルロジックは省略
return (
);
}
*/
// src/components/ThemeSwitcher.js (既に例示済み)
/*
import React, { useContext } from ‘react’;
import { ThemeContext } from ‘../contexts/ThemeContext’;
function ThemeSwitcher() {
const { theme, toggleTheme } = useContext(ThemeContext);
return ();
}
*/
``
App
この例では、コンポーネントでテーマの状態と切り替え関数を管理し、
ThemeContext.Providerを通じて子孫コンポーネントにこれらの値を渡しています。
ThemeSwitcherと
ThemedComponentは、コンポーネントツリーのどこに配置されていても、
useContext(ThemeContext)`を呼び出すだけでテーマの値や切り替え関数にアクセスできます。
例5: カスタムHooksを使ったフォーム入力バリデーション
複数の入力フィールドを持つフォームで、入力値のステート管理と簡単なバリデーションロジックをカスタムHooksとして切り出す例です。
“`javascript
// src/hooks/useFormInput.js
import { useState } from ‘react’;
function useFormInput(initialValue, validator = (val) => true) {
const [value, setValue] = useState(initialValue);
const [isValid, setIsValid] = useState(validator(initialValue));
const [isTouched, setIsTouched] = useState(false);
const handleChange = (event) => {
const newValue = event.target.value;
setValue(newValue);
setIsValid(validator(newValue)); // 値が変わるたびにバリデーション
};
const handleBlur = () => {
setIsTouched(true); // フォーカスが外れたら触られた状態にする
};
// 入力値、バリデーション状態、変更ハンドラー、ブラーハンドラーを返す
return {
value,
isValid,
isTouched,
handleChange,
handleBlur,
reset: (newValue = initialValue) => { // リセット機能も追加
setValue(newValue);
setIsValid(validator(newValue));
setIsTouched(false);
}
};
}
export default useFormInput;
“`
カスタムHooksの利用例:
“`javascript
import React from ‘react’;
import useFormInput from ‘../hooks/useFormInput’; // カスタムHooksをインポート
// 簡単なバリデーション関数
const required = (val) => val.trim() !== ”;
const minLength = (len) => (val) => val.length >= len;
const isEmail = (val) => /\S+@\S+.\S+/.test(val);
function UserSignupForm() {
// カスタムHooksを使って各入力フィールドの状態を管理
const firstName = useFormInput(”, required);
const lastName = useFormInput(”, required);
const email = useFormInput(”, isEmail);
const password = useFormInput(”, minLength(8));
// フォーム全体のバリデーション状態
const isFormValid = firstName.isValid && lastName.isValid && email.isValid && password.isValid;
const handleSubmit = (event) => {
event.preventDefault();
// フォームが有効か確認
if (isFormValid) {
console.log(‘Form submitted:’, {
firstName: firstName.value,
lastName: lastName.value,
email: email.value,
password: password.value
});
// 送信後にフォームをリセット
firstName.reset(”);
lastName.reset(”);
email.reset(”);
password.reset(”);
} else {
alert(‘Please fill out the form correctly.’);
}
};
return (
);
}
``
useFormInput
この例では、というカスタムHooksを作成し、各入力フィールドの値をステートで管理し、バリデーションを行うロジックをカプセル化しています。
UserSignupFormコンポーネントは、
useFormInput`を呼び出すだけでフォーム入力のロジックを簡単に利用でき、各フィールドの入力値、バリデーション状態、およびハンドラーを取得できます。これにより、フォームのステート管理とバリデーションロジックが再利用可能になり、コンポーネント自体はシンプルに保たれます。
9. まとめと今後の展望
本記事では、Reactの関数コンポーネント(FC)に焦点を当て、その基本的な定義から、Propsの扱い、そしてHooksを使ったステート管理 (useState
)、副作用の処理 (useEffect
)、コンテキストの利用 (useContext
)、永続的な値の保持 (useRef
)、複雑なステート管理 (useReducer
)、パフォーマンス最適化のためのメモ化 (useCallback
, useMemo
)、さらにはカスタムHooksによるロジックの再利用まで、幅広く詳細に解説しました。
Hooksの登場は、Reactの開発体験を大きく向上させました。関数コンポーネントだけで、かつてクラスコンポーネントでしか実現できなかった多くの機能を手軽に、そしてより柔軟に扱えるようになったのです。これにより、コンポーネントの記述がより関数的で予測可能になり、ロジックの再利用性が高まりました。
現代のReact開発において、関数コンポーネントとHooksはまさに中心的な存在です。これからReactを学ぶ方にとっても、既存のReactコードを理解・改善したい方にとっても、関数コンポーネントとHooksを深く理解することは必須と言えるでしょう。
学ぶべき次のステップとしては、以下のトピックが挙げられます。
- Context APIと
useContext
のより高度な利用: 大規模なアプリケーションでのContextの設計。 useReducer
の詳細と実践: より複雑なステート管理パターン。- カスタムHooksの多様なパターン: データ取得、フォーム管理以外のロジックを切り出す方法。
- サードパーティ製Hooks: React RouterのHooks、ステート管理ライブラリ(Recoil, Zustand, Jotaiなど)が提供するHooksなど。
- エラーバウンダリ: 関数コンポーネントでは直接定義できないエラーバウンダリの概念と、クラスコンポーネントやライブラリを使った実装方法。
- SuspenseとConcurrent Features: データフェッチやコード分割における新たなHooks (
useDeferredValue
,useTransition
など) と、それらを支えるReactの並行処理機能。 - Server Components: サーバーサイドでコンポーネントをレンダリングし、クライアントとの連携を最適化する新しいパラダイムと関数コンポーネントの関わり。
Reactは常に進化しています。しかし、関数コンポーネントとHooksという基盤は今後もReact開発の核であり続けるでしょう。本記事が、皆さんのReact開発における関数コンポーネントの理解を深め、より効果的な開発を進めるための一助となれば幸いです。
Hooksのルールを守り、各Hooksの目的と適切な使い方を理解することで、クリーンで保守しやすく、パフォーマンスの高いReactアプリケーションを構築できるようになるはずです。是非、実際に手を動かして、Hooksの力を体験してみてください。