関数コンポーネントとクラスの比較
最後にクラスと関数コンポーネントを比較します。細部に目をつむるとstateはクラスのプロパティ相当であり、状態の更新関数を実行するイベントハンドラはメソッド相当として考えられると思います。
オブジェクト指向のクラス
第1回で提示した記事クラスのeditTitle()メソッドを思い出してみてください。
editTitle(newTitle: Title) {
if (this.status === Status.Archived) {
throw new Error('Archived な記事のタイトルは編集できません');
}
this.title = newTitle;
}
記事ステータスは公開済みとアーカイブ済みの2種類です。
const Status = {
Published: 'published',
Archived: 'archived',
} as const;
editTitle()を呼び出したとき、記事が公開済みならタイトルを編集できますが、記事がアーカイブされていたら編集できません。記事ステータスという名前が示す通り、これは記事の「状態」に応じた挙動なのです。
archive() {
this.status = Status.Archived;
}
第1回で例に挙げたArticleクラスをReactのクラスコンポーネントとして表現してみました。本章の冒頭で「state はクラスのプロパティ相当、イベントハンドラはメソッド相当」と書いた意味がわかってもらえるかと思います。
import React, { Component } from 'react';
type ArticleProps = {
articleId: string;
title: string;
content: string;
author: string;
status: 'Published' | 'Archived';
publishedAt: Date;
};
type ArticleState = {
title: string;
content: string;
status: 'Published' | 'Archived';
};
export default class Article extends Component<ArticleProps, ArticleState> {
constructor(props: ArticleProps) {
super(props);
this.state = {
title: props.title,
content: props.content,
status: props.status,
};
}
editTitle(newTitle: string) {
if (this.state.status === 'Archived') {
throw new Error('Archived な記事のタイトルは編集できません');
}
this.setState({ title: newTitle });
}
archive() {
this.setState({ status: 'Archived' });
}
render() {
const { author, publishedAt } = this.props;
const { title, content, status } = this.state;
return (
<article>
<header>
<h2>{title}</h2>
<p>
著者: {author}(公開日: {publishedAt.toLocaleDateString()})
</p>
</header>
<p>{content}</p>
<p>ステータス: {status}</p>
<div>
<button onClick={() => this.editTitle('新しいタイトル')}>
タイトルを編集
</button>
<button
onClick={() => {
try {
this.archive();
} catch (e) {
alert(e.message);
}
}}
disabled={status === 'Archived'}
>
アーカイブする
</button>
</div>
</article>
);
}
}
コンポーネント指向のReactコンポーネント
ここからはもう少し複雑なコンポーネントの説明をします。より実践的なコンポーネントであるため、わからなければ次のクラスとコンポーネントの挙動の比較まで読み飛ばしてもらっても構いません。
オブジェクト指向のArticleクラスのメソッドが呼び出されると、処理内容はプロパティの値に応じて条件分岐するのと同じようなことをReactコンポーネントで表現してみます。
以下のArticleWithStatusコンポーネントはstatusという状態と記事を「アーカイブする」ボタンを持ちます。このボタンをクリックすると、コンポーネントのstatusがarchivedに変更され、同時にバックエンドのAPIにリクエストが飛びます。
アーカイブ済みの記事は「アーカイブする」ボタンを押せないという仕様だとします。ステータスがarchivedの時はそもそもボタンをクリックできないようにしたいため、isArchivedという変数を作ってボタンのdisabled属性に渡します。
type ArticleStatus = 'published' | 'archived';
type Props = {
articleStatus: ArticleStatus;
};
function ArticleWithStatus({ articleStatus }: Props) {
// Article クラスの status 相当
const [status, setStatus] = useState<ArticleStatus>(articleStatus);
// ハンドラ
const handleClickArchive = async () => {
if (status === 'archived') {
return;
}
// status を archived に更新する。
// Article クラスの Article.archive() 相当
setStatus('archived');
// APIサーバーにリクエストを送る
};
// アーカイブ状態か確認するフラグ
const isArchived = status === 'archived';
return (
<div>
// ボタンに disabled 属性を付与することにより
// 「アーカイブする」ボタンは公開済みステータスの時にしか押せないようにする
<button onClick={handleClickArchive} disabled={isArchived}>
アーカイブする
</button>
<h2>記事タイトル</h2>
// ...
</div>
);
}
クラスメソッドは、条件分岐でプロパティの値に応じて処理内容を変えることが一般的です。それと同様に、Reactコンポーネントもstateの値が変更されることでJSXを変え、描画されるHTMLを変更するのです。まとめると以下の動きをします。
ArticleクラスのeditTitle()メソッドの挙動
- Statusプロパティが published のとき、editTitle() で記事タイトルを変更できる
- Statusプロパティが archived のとき、editTitle() は例外を投げる
ArticleWithStatusコンポーネントの挙動
-
statusステートが published のとき、ボタンのdisabledがfalseであるJSXを返す
- 「アーカイブ」するボタンを押すと、statusステートが archived に更新される(ユーザーのアクションによる再描画)
- statusステートがarchivedのとき、ボタンのdisabledがtrueであるJSXを返す
一般的にメソッドを設計するときは次のようなテンプレートに基づいて記述すると良いとされています[2]。
- 事前条件のチェック
- 失敗のシナリオ
- ハッピーパス(正常系)
- 事後条件のチェック
- voidか値を返す
これをReactコンポーネント風に書くとこのようになるでしょう。
- state の設定
- イベントハンドラ
- プレゼンテーションロジック
- JSXを返す
コンポーネントの役割は「イベントハンドラのロジックとプレゼンテーションのロジック」であるため、この流れがコンポーネント記述の基本です。ただし、他にも副作用やAPI経由のデータフェッチ、フロントエンドでのルーティングなどもあるため、これはあくまで基本です。
まとめ
次回はAPIサーバーから取得したデータを表示するまでの流れ(データフェッチ)について紹介します。
- [2]出典:『オブジェクト指向設計スタイルガイド』p.151
