メインコンテンツまでスキップ

クラスコンポーネント

この章では、Reactのクラスコンポーネントについて学びます。現在のReact開発では関数コンポーネントが主流ですが、既存のコードベースの理解やError Boundaryの実装のために知識が必要です。

この章で学ぶこと

  • クラスコンポーネントの基本構文
  • ライフサイクルメソッドとHooksの対応関係
  • Error Boundaryの実装
  • クラスから関数コンポーネントへの移行方法
備考

この章では03章で作成したプロジェクトを使用します。npm run devで開発サーバーを起動し、コードを書きながら学習を進めましょう。

なぜクラスコンポーネントを学ぶのか

  1. レガシーコードの理解: 既存プロジェクトにはクラスコンポーネントが含まれている可能性
  2. Error Boundary: クラスコンポーネントでのみ実装可能
  3. ライフサイクルの理解: Reactの動作原理を深く理解できる
備考

新規開発では関数コンポーネント + Hooksを使用してください。クラスコンポーネントは特別な理由がない限り推奨されません。

クラスコンポーネントの基本

基本構文

import { Component } from 'react'

type GreetingProps = {
name: string
}

class Greeting extends Component<GreetingProps> {
render() {
return <h1>Hello, {this.props.name}!</h1>
}
}

// 使用
<Greeting name="React" />

関数コンポーネントとの比較

// クラスコンポーネント
class ClassCounter extends Component<{}, { count: number }> {
state = { count: 0 }

increment = () => {
this.setState({ count: this.state.count + 1 })
}

render() {
return (
<div>
<p>{this.state.count}</p>
<button onClick={this.increment}>+1</button>
</div>
)
}
}

// 関数コンポーネント(推奨)
function FunctionCounter() {
const [count, setCount] = useState(0)

return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
)
}

State

クラスコンポーネントでのState管理です。

State の初期化

class Counter extends Component<{}, { count: number; name: string }> {
// 方法1: クラスフィールドで初期化(推奨)
state = {
count: 0,
name: ''
}

// 方法2: コンストラクタで初期化
// constructor(props: {}) {
// super(props)
// this.state = { count: 0, name: '' }
// }

render() {
return <p>{this.state.count}</p>
}
}

setState

class Counter extends Component<{}, { count: number }> {
state = { count: 0 }

// オブジェクトを渡す方法
increment = () => {
this.setState({ count: this.state.count + 1 })
}

// 関数を渡す方法(前の状態に依存する場合に推奨)
incrementSafe = () => {
this.setState(prevState => ({
count: prevState.count + 1
}))
}

// 複数回呼び出しの違い
incrementMultiple = () => {
// ❌ 3回呼んでも1しか増えない(バッチ処理される)
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })

// ✅ 3増える
this.setState(prev => ({ count: prev.count + 1 }))
this.setState(prev => ({ count: prev.count + 1 }))
this.setState(prev => ({ count: prev.count + 1 }))
}

render() {
return (
<button onClick={this.incrementSafe}>
{this.state.count}
</button>
)
}
}
警告

this.setStateは非同期的に処理されます。更新後の値をすぐに使いたい場合は、第2引数のコールバックを使用します。

this.setState({ count: 10 }, () => {
console.log('Updated count:', this.state.count) // 10
})

ライフサイクルメソッド

クラスコンポーネントには、コンポーネントの生成から破棄までの各段階で呼び出されるメソッドがあります。

ライフサイクルの流れ

マウント 更新 アンマウント
-------- ---- ----------
constructor() getDerivedStateFromProps
↓ ↓
getDerivedStateFromProps shouldComponentUpdate()
↓ ↓
render() render()
↓ ↓
componentDidMount() componentDidUpdate() componentWillUnmount()

componentDidMount

コンポーネントがDOMにマウントされた直後に呼ばれます。useEffectの空依存配列に相当します。

class UserProfile extends Component<{ userId: string }, { user: User | null }> {
state: { user: User | null } = { user: null }

componentDidMount() {
// データフェッチ、サブスクリプション、DOM操作など
this.fetchUser()
}

fetchUser = async () => {
const user = await fetchUser(this.props.userId)
this.setState({ user })
}

render() {
const { user } = this.state
if (!user) return <p>Loading...</p>
return <p>{user.name}</p>
}
}

クラスフィールドで null を初期値にする場合は、上記のように state フィールドへ明示的な型注釈を付けてください。注釈がないと state の型が初期化子から { user: null } と推論され、user.name の参照が型エラーになります。

componentDidUpdate

更新が発生した直後に呼ばれます。useEffectの依存配列ありに相当します。

class UserProfile extends Component<{ userId: string }, { user: User | null }> {
state: { user: User | null } = { user: null }

componentDidMount() {
this.fetchUser()
}

componentDidUpdate(prevProps: { userId: string }) {
// propsが変わった場合のみ再フェッチ
if (prevProps.userId !== this.props.userId) {
this.fetchUser()
}
}

fetchUser = async () => {
const user = await fetchUser(this.props.userId)
this.setState({ user })
}

render() {
// ...
}
}
警告

componentDidUpdate内でsetStateを呼ぶ場合は、必ず条件分岐を入れてください。そうしないと無限ループになります。

componentWillUnmount

コンポーネントがアンマウントされる直前に呼ばれます。useEffectのクリーンアップ関数に相当します。

class Timer extends Component<{}, { seconds: number }> {
state = { seconds: 0 }
timerId: number | null = null

componentDidMount() {
this.timerId = window.setInterval(() => {
this.setState(prev => ({ seconds: prev.seconds + 1 }))
}, 1000)
}

componentWillUnmount() {
// リソースのクリーンアップ
if (this.timerId) {
clearInterval(this.timerId)
}
}

render() {
return <p>{this.state.seconds}</p>
}
}

ライフサイクルと Hooks の対応

なぜこの対応表を理解すべきか

既存のクラスコンポーネントを関数コンポーネントに移行する際、この対応関係を理解していると作業がスムーズになります。また、ライブラリのサンプルコードがクラスコンポーネントで書かれている場合にも、関数コンポーネントへの変換が容易になります。

クラスコンポーネント関数コンポーネント (Hooks)
componentDidMountuseEffect(() => {}, [])
componentDidUpdateuseEffect(() => {}, [deps])
componentWillUnmountuseEffect(() => { return () => {} }, [])
this.stateuseState
this.setStatesetState関数
// クラスコンポーネント
class Example extends Component {
state = { data: null }

componentDidMount() {
fetchData().then(data => this.setState({ data }))
}

componentWillUnmount() {
cleanup()
}

render() {
return <div>{this.state.data}</div>
}
}

// 関数コンポーネント(等価)
function Example() {
const [data, setData] = useState(null)

useEffect(() => {
fetchData().then(setData)
return () => cleanup()
}, [])

return <div>{data}</div>
}

Error Boundary

Error Boundaryは、子コンポーネントツリーで発生したJavaScriptエラーをキャッチし、フォールバックUIを表示する機能です。

基本的なError Boundary

import { Component, type ReactNode, type ErrorInfo } from 'react'

type ErrorBoundaryProps = {
children: ReactNode
fallback?: ReactNode
}

type ErrorBoundaryState = {
hasError: boolean
error: Error | null
}

class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { hasError: false, error: null }
}

// エラーが発生したときにstateを更新
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}

// エラー情報をログ
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught:', error)
console.error('Component stack:', errorInfo.componentStack)

// エラー追跡サービスに送信
// logErrorToService(error, errorInfo)
}

render() {
if (this.state.hasError) {
return this.props.fallback ?? (
<div className="p-4 bg-red-50 border border-red-200 rounded">
<h2 className="text-red-700 font-bold">エラーが発生しました</h2>
<p className="text-red-600">{this.state.error?.message}</p>
</div>
)
}

return this.props.children
}
}

export default ErrorBoundary

Error Boundaryの使用

function App() {
return (
<div>
<ErrorBoundary fallback={<p>ヘッダーの読み込みに失敗しました</p>}>
<Header />
</ErrorBoundary>

<ErrorBoundary fallback={<p>メインコンテンツの読み込みに失敗しました</p>}>
<Main />
</ErrorBoundary>

<Footer />
</div>
)
}

リセット機能付きError Boundary

type ErrorBoundaryProps = {
children: ReactNode
onReset?: () => void
}

type ErrorBoundaryState = {
hasError: boolean
error: Error | null
}

class ResettableErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { hasError: false, error: null }
}

static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error:', error, errorInfo)
}

handleReset = () => {
this.setState({ hasError: false, error: null })
this.props.onReset?.()
}

render() {
if (this.state.hasError) {
return (
<div className="p-4 bg-red-50 border border-red-200 rounded">
<h2 className="text-red-700 font-bold">エラーが発生しました</h2>
<p className="text-red-600 mb-4">{this.state.error?.message}</p>
<button
onClick={this.handleReset}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
再試行
</button>
</div>
)
}

return this.props.children
}
}

Error Boundaryがキャッチできないエラー

以下のエラーはError Boundaryではキャッチできません。

// ❌ イベントハンドラ内のエラー
<button onClick={() => { throw new Error('Click error') }}>
クリック
</button>

// ❌ 非同期コード(setTimeout, Promise)
useEffect(() => {
setTimeout(() => {
throw new Error('Async error')
}, 1000)
}, [])

// ❌ サーバーサイドレンダリング

// ❌ Error Boundary自身で発生したエラー

イベントハンドラや非同期コードのエラーは、try-catchで個別にハンドリングします。

function SaveButton() {
const handleSave = async () => {
try {
await saveData()
} catch (error) {
// エラーハンドリング
alert('保存に失敗しました')
}
}

return <button onClick={handleSave}>保存</button>
}

クラスコンポーネントのパターン

メソッドのバインド

class Example extends Component {
// 方法1: コンストラクタでバインド
constructor(props: {}) {
super(props)
this.handleClick = this.handleClick.bind(this)
}

handleClick() {
console.log(this) // コンポーネントインスタンス
}

// 方法2: アロー関数(推奨)
handleClickArrow = () => {
console.log(this) // コンポーネントインスタンス
}

render() {
return (
<div>
<button onClick={this.handleClick}>Click</button>
<button onClick={this.handleClickArrow}>Click</button>
</div>
)
}
}

defaultPropsとpropTypes

import PropTypes from 'prop-types'

class Button extends Component<{ label?: string; onClick?: () => void }> {
static defaultProps = {
label: 'Click me'
}

// propTypes は React 18 以前のレガシー記法(React 19 では指定しても黙って無視される)
static propTypes = {
label: PropTypes.string,
onClick: PropTypes.func
}

render() {
return <button onClick={this.props.onClick}>{this.props.label}</button>
}
}
備考

React 19 では propTypes のチェック機構自体が React 本体から削除され、指定しても黙って無視されます。型チェックは TypeScript で行ってください。なお static defaultProps はクラスコンポーネントでは引き続き動作します(関数コンポーネントでは削除済みのため、ES6 のデフォルト引数を使います)。

クラスから関数への移行

既存のクラスコンポーネントを関数コンポーネントに移行する例です。

Before: クラスコンポーネント

class UserList extends Component<{ filter: string }, { users: User[]; loading: boolean }> {
state: { users: User[]; loading: boolean } = {
users: [],
loading: true
}

componentDidMount() {
this.fetchUsers()
}

componentDidUpdate(prevProps: { filter: string }) {
if (prevProps.filter !== this.props.filter) {
this.fetchUsers()
}
}

fetchUsers = async () => {
this.setState({ loading: true })
const users = await fetchUsers(this.props.filter)
this.setState({ users, loading: false })
}

render() {
const { users, loading } = this.state

if (loading) return <p>Loading...</p>

return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
}

After: 関数コンポーネント

function UserList({ filter }: { filter: string }) {
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(true)

useEffect(() => {
const fetchData = async () => {
setLoading(true)
const data = await fetchUsers(filter)
setUsers(data)
setLoading(false)
}

fetchData()
}, [filter])

if (loading) return <p>Loading...</p>

return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}

試してみよう

以下の課題に挑戦して、クラスコンポーネントの理解を深めましょう。

  1. クラスコンポーネントでカウンターを作る: useStateを使わずに、クラスコンポーネントで「+1」「-1」「リセット」ボタンを持つカウンターを作成してください。

  2. ライフサイクルを体験する: componentDidMountcomponentDidUpdatecomponentWillUnmountそれぞれにconsole.logを追加し、コンポーネントのマウント・更新・アンマウント時にログが出力されることを確認してください。

  3. Error Boundaryを実装する: リセット機能付きのError Boundaryを自分で実装し、意図的にエラーを発生させるコンポーネントをラップしてください。「再試行」ボタンでエラー状態がリセットされることを確認しましょう。

ヒント
  1. state = { count: 0 }で初期化し、this.setState({ count: this.state.count + 1 })で更新します。リセットは{ count: 0 }を設定します。
  2. 親コンポーネントで子を条件付きレンダリングし、表示/非表示を切り替えるとcomponentWillUnmountが呼ばれます。
  3. getDerivedStateFromErrorでエラー状態を設定し、handleResetメソッドでhasError: falseに戻します。
回答と解説

課題1: クラスコンポーネントでカウンターを作る

import { Component } from 'react'
import './App.css'

class Counter extends Component<{}, { count: number }> {
state = { count: 0 }

increment = () => {
this.setState({ count: this.state.count + 1 })
}

decrement = () => {
this.setState({ count: this.state.count - 1 })
}

reset = () => {
this.setState({ count: 0 })
}

render() {
return (
<div className="p-4">
<p className="text-2xl mb-4">{this.state.count}</p>
<div className="space-x-2">
<button onClick={this.decrement} className="px-4 py-2 bg-red-500 text-white rounded">
-1
</button>
<button onClick={this.reset} className="px-4 py-2 bg-gray-500 text-white rounded">
リセット
</button>
<button onClick={this.increment} className="px-4 py-2 bg-blue-500 text-white rounded">
+1
</button>
</div>
</div>
)
}
}

export default function App() {
return <Counter />
}

クラスコンポーネントでは、stateプロパティで状態を定義し、this.setState()で更新します。メソッドはアロー関数で定義するとthisのバインドが自動的に行われます。関数コンポーネントのuseStateと比較すると、状態の定義と更新の方法が異なることがわかります。


課題2: ライフサイクルを体験する

import { Component, useState } from 'react'
import './App.css'

// 子コンポーネント(ライフサイクルを持つ)
class LifecycleDemo extends Component<{ name: string }, { count: number }> {
state = { count: 0 }

componentDidMount() {
console.log('componentDidMount: コンポーネントがマウントされました')
}

componentDidUpdate(prevProps: { name: string }, prevState: { count: number }) {
console.log('componentDidUpdate: コンポーネントが更新されました')
console.log(' 前のprops:', prevProps, '→ 現在:', this.props)
console.log(' 前のstate:', prevState, '→ 現在:', this.state)
}

componentWillUnmount() {
console.log('componentWillUnmount: コンポーネントがアンマウントされます')
}

render() {
return (
<div className="p-4 border rounded">
<p>名前: {this.props.name}</p>
<p>カウント: {this.state.count}</p>
<button
onClick={() => this.setState({ count: this.state.count + 1 })}
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
>
カウントアップ
</button>
</div>
)
}
}

// 親コンポーネント(表示/非表示を制御)
function App() {
const [isVisible, setIsVisible] = useState(true)

const handleToggle = () => {
setIsVisible(!isVisible)
}

return (
<div className="p-4">
<button
onClick={handleToggle}
className="mb-4 px-4 py-2 bg-gray-500 text-white rounded"
>
{isVisible ? '非表示にする' : '表示する'}
</button>
{isVisible && <LifecycleDemo name="React" />}
</div>
)
}

export default App

コンソールを開いて以下を確認してください:

  • ページ読み込み時にcomponentDidMountが出力される
  • カウントアップボタンを押すとcomponentDidUpdateが出力される
  • 「非表示にする」を押すとcomponentWillUnmountが出力される

課題3: Error Boundaryを実装する

import { Component, type ReactNode, type ErrorInfo, useState } from 'react'
import './App.css'

// Error Boundary
type ErrorBoundaryProps = {
children: ReactNode
onReset?: () => void
}

type ErrorBoundaryState = {
hasError: boolean
error: Error | null
}

class ResettableErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { hasError: false, error: null }
}

static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught by boundary:', error)
console.error('Component stack:', errorInfo.componentStack)
}

handleReset = () => {
this.setState({ hasError: false, error: null })
this.props.onReset?.()
}

render() {
if (this.state.hasError) {
return (
<div className="p-4 bg-red-50 border border-red-200 rounded">
<h2 className="text-red-700 font-bold">エラーが発生しました</h2>
<p className="text-red-600 mb-4">{this.state.error?.message}</p>
<button
onClick={this.handleReset}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
再試行
</button>
</div>
)
}
return this.props.children
}
}

// 意図的にエラーを発生させるコンポーネント
function BuggyComponent() {
const [shouldError, setShouldError] = useState(false)

const handleTriggerError = () => {
setShouldError(true)
}

if (shouldError) {
throw new Error('意図的に発生させたエラーです')
}

return (
<div className="p-4 border rounded">
<p>正常に動作しています</p>
<button
onClick={handleTriggerError}
className="mt-2 px-4 py-2 bg-red-500 text-white rounded"
>
エラーを発生させる
</button>
</div>
)
}

// 使用例
function App() {
const [key, setKey] = useState(0)

return (
<div className="p-4">
<ResettableErrorBoundary onReset={() => setKey(k => k + 1)}>
<BuggyComponent key={key} />
</ResettableErrorBoundary>
</div>
)
}

export default App

「エラーを発生させる」ボタンをクリックするとエラーがキャッチされ、フォールバックUIが表示されます。「再試行」をクリックするとhandleResetが呼ばれ、hasError: falseに戻ります。keyを変更することでコンポーネントを完全にリセットしています。

まとめ

  • クラスコンポーネントは既存コードの理解とError Boundaryのために必要
  • this.statethis.setStateで状態管理
  • ライフサイクルメソッド(componentDidMount, componentDidUpdate, componentWillUnmount)
  • Error Boundaryでレンダリングエラーをキャッチ
  • 新規開発では関数コンポーネント + Hooksを使用

次の章では、Next.jsによるサーバーサイドレンダリングと最新のReact機能について学びます。

次に読む