まくろぐ

React コンポーネント実装の基本(関数コンポーネントとクラスコンポーネント)

更新:
作成:

React で独自コンポーネントを作成する方法として、大きく次の 2 種類の方法があります。

  • 関数コンポーネント (Function Components)
  • クラスコンポーネント (Class Components)

与えられた値を単純に表示するだけであれば、関数コンポーネントがお手軽です。 ステートレスなコンポーネントであれば、できるだけ関数コンポーネントとして作成することが推奨されています。 コンポーネントにビューの責務だけを持たせることにより、データ処理部分と疎な関係を保つことができます。

一方、状態 (state) を持ち、状態の変化に応じて表示内容を変化させたい場合は、クラスコンポーネントを使用します(正確に言うと、React の Hook の仕組みを使うと、関数コンポーネントにも状態を持たせることができます)。

関数コンポーネント

関数コンポーネントの基本

下記は、固定のテキストを表示するシンプルな関数コンポーネントの定義例です。 TypeScript (@types/react) では、関数コンポーネントの型は React.FunctionComponent インタフェースとして定義されています。 エイリアスとして React.FC が定義されているので、こちらを使えばより短く記述できます。

components/hello.tsx
import * as React from 'react';

// Hello コンポーネントの定義
export const Hello: React.FC = () => {
  return <h1>Hello</h1>
};

HTML ファイルから読み込む JS ファイルでは、ReactDOM.render() で上記の Hello コンポーネントを描画します。 次のコードを実行すると、<div id="root"> 要素の内容が、Hello コンポーネントの内容に置き換えられます。

index.tsx(使用例)
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Hello } from './components/hello';

ReactDOM.render(<Hello />, document.getElementById('root'));

一見すると、1 行目の React モジュールのインポートは必要ないように見えますが、JSX コードが変換されると React を参照するコードになるので、この行を消してはいけません。

プロパティを定義する (props)

関数コンポーネントは状態 (state) を持ちませんが、関数のパラメータとして、表示すべき値(プロパティ: props)を受け取ることができます。 このプロパティには、HTML 要素でいうところの「属性」として指定された値が格納されています。 TypeScript を使っているのであれば、プロパティの型をちゃんと定義して、React.FC の型パラメータとして指定します。 次の例では、2 つのプロパティ(nameage)を受け取る関数コンポーネントを定義しています。

components/hello.tsx
import * as React from 'react';

// Hello コンポーネントのプロパティ
export interface HelloProps {
  name: string;
  age: number;
}

// Hello コンポーネントの定義
export const Hello: React.FC<HelloProps> = (props) => {
  return <h1>私は{props.name}です。{props.age}歳です。</h1>
};
index.tsx(使用例)
ReactDOM.render(
  <Hello name="まく" age={14} />,
  document.getElementById('root')
);

私はまくです。14歳です。 と表示されれば成功です。

プロパティをオプショナルにする

あるプロパティをオプションにしたいときは、TypeScript の型定義をするときに、プロパティ名の末尾に ? を付けます。 次の例では、age プロパティの指定をオプショナルにしています。

components/hello.ts
import * as React from 'react';

// Hello コンポーネントのプロパティ
export interface HelloProps {
  name: string;
  age?: number;
}

// Hello コンポーネントの定義
export const Hello: React.FC<HelloProps> = (props) => {
  const ageText = props.age ? `${props.age}歳` : '秘密';
  return <h1>私は{props.name}です。年齢は{ageText}です。</h1>
};

プロパティのデフォルト値を設定する

React.FCdeafultProps で、プロパティのデフォルト値を定義しておくことができます。 次の例では、Hello コンポーネントの nameage をオプショナルプロパティとして定義し、それぞれ指定されなかった場合のデフォルト値を設定しています。

components/hello.tsx
import * as React from 'react';

export interface HelloProps {
  name?: string;
  age?: number;
}

export const Hello: React.FC<HelloProps> = (props) => {
  return <h1>私は{props.name}です。{props.age}歳です。</h1>
};

// プロパティのデフォルト値
Hello.defaultProps = { name: '名無し', age: 0 };

defaultProps を使わずに、TypeScript の ?? 構文を使ってデフォルト値を設定してしまう方法もあります。 実はこっちの方が柔軟性があるかも。

export const Hello: React.FC<HelloProps> = (props) => {
  const name = props.name ?? '名無し';
  const age = props.age ?? 0;
  return <h1>私は{name}です。{age}歳です。</h1>
};

クラスコンポーネント

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

React コンポーネントをクラスの形で定義することもできます。 TypeScript であれば、React.Component を継承し、render() メソッドを実装します。 コンポーネントにプロパティを持たせることができるのは、関数コンポーネントの場合と同様です。

components/hello.tsx
import * as React from 'react';

export interface HelloProps {
  name: string;
}

export class Hello extends React.Component<HelloProps> {
  public render() : JSX.Element {
    return <h1>私は{this.props.name}です。</h1>
  }
};
index.tsx(使用例)
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Hello } from './components/hello';

ReactDOM.render(<Hello name="まく" />, document.getElementById('root'));

注意点として、関数コンポーネントのときに関数のパラメータで受け取っていたプロパティ (props) は、クラスコンポーネントの場合はメンバ変数(こちらをプロパティと呼ぶと紛らわしい^^;)として参照するというところです。 つまり、this を付けて、this.props.プロパティ名 という形で参照する必要があります。

プロパティのデフォルト値を設定する

クラスコンポーネントのオプショナルなプロパティにデフォルト値を設定するには、次のように static な defaultPorps を定義します。

components/hello.tsx
import * as React from 'react';

export interface HelloProps {
  name?: string;
}

export class Hello extends React.Component<HelloProps> {
  private static defaultProps: HelloProps = {
    name: '名無し'
  }

  public render() : JSX.Element {
    return <h1>私は{this.props.name}です。</h1>
  }
};

プロパティの一部だけがオプショナルになっている場合は、defaultProps の型は HelloProps ではなく、ES5 の Partial を使って Partial<HelloProps> のように定義する必要があります(こうしないと、必須プロパティの name が指定されていないと言うエラーになります)。 次の例では、プロパティ age だけをオプショナルにし、そのデフォルト値を 0 に設定しています。

components/hello.tsx
import * as React from 'react';

export interface HelloProps {
  name: string;
  age?: number;
}

export class Hello extends React.Component<HelloProps> {
  private static defaultProps: Partial<HelloProps> = {
    age: 0
  }

  public render() : JSX.Element {
    return <h1>私は{this.props.name}です。{this.props.age}歳です。</h1>
  }
};

と、ここまで defaultProps を使う方法を説明しましたが、実は次のようにプロパティの値を見て条件分岐してしまった方が簡単です。

public render() : JSX.Element {
  const age = this.props.age ?? 0;
  return <h1>私は{this.props.name}です。{age}歳です。</h1>
}

状態を持たせる (state)

クラスコンポーネントには、状態を持たせることができ、その値を使って描画内容を動的に変更することができます。 状態の型は、React.Component の 2 番目の型パラメータとして指定します(1 番目はプロパティの型)。 現在の状態は、インスタンスプロパティ state によって管理します。 state の値は、コンストラクタで初期化できます。

次の例では、ボタンを押すごとにカウントアップする CountButton コンポーネントを定義しています。

/p/vfr3cnw/img-001.png
図: CountButton の表示結果
components/countButton.tsx
import * as React from 'react';

// CountButton コンポーネントの状態の型
interface CountButtonState {
  count: number;
}

// CountButton コンポーネントの定義
export class CountButton extends React.Component<{}, CountButtonState> {
  constructor(props: {}) {
    super(props);
    this.state = { count: 0 };  // 状態を初期化
  }

  public render(): JSX.Element {
    return (
      <div>
        <button onClick={this.handleClick}>増やす</button>
        <b>カウント = {this.state.count}</b>
      </div>
    );
  }

  // ボタンが押されたときの処理
  private handleClick = (e: React.SyntheticEvent) => {
    // デフォルト動作を抑制したい場合
    e.preventDefault();

    // コンポーネントの状態を変更する → 新しい状態で render() が実行される
    this.setState({
      count: this.state.count + 1
    });
  }
}
index.tsx(使用例)
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { CountButton } from './components/countButton';

ReactDOM.render(<CountButton />, document.getElementById('root'));

いくつか実装上のポイントがあるので、まとめておきます。

  • コンストラクタで props を受け取る: クラスコンポーネントのコンストラクタを定義するときは、パラメータとしてプロパティ (props) を受け取り、親クラス (super) に渡してやる必要があります。上記の CountButton コンポーネントはプロパティを持たないので、型パラメータとしては {} を指定していますが、その場合でもコンストラクタのパラメータでは props を受け取るように実装しておく必要があります。
  • render 関数の return 直後に開き括弧: return の直後に改行を入れると、JavaScript が自動的にセミコロンを挿入してしまうので、すぐに改行したいときは return ( のように、開き括弧の後ろで改行する必要があります。
  • onClick と handleClick: JSX コードの中で配置したボタンのクリックイベントをハンドルするには、onClick という属性にイベントハンドラを指定します。イベントハンドラ名は handleClick のように、名前に handle というプレフィックスを付けるのが慣例となっています。
  • イベントハンドラはアロー関数で: this で呼び出し元のオブジェクトを参照できるように、handleClick メソッドは function キーワードを使わずに、アロー関数の形で定義します。
  • デフォルト動作を抑止する: button 要素などのクリック時のデフォルト動作(フォーム内容の post など)を抑制するには、イベントハンドラのパラメータで渡される React.SyntheticEvent オブジェクトの preventDefault() メソッドを呼び出します。
  • 状態の変更は setState 関数で: state プロパティはリードオンリーになっているので、コンポーネントの状態を変更したいときは、setState() 関数を使用します(コンストラクタでは例外的に代入できます)。setState() を使って状態を変更すると、自動的に render() メソッドが呼び出され、新しい状態 (state) で画面上の表示が更新されます。

関連記事

まくろぐ
サイトマップまくへのメッセージ