React:関数コンポーネントとクラスコンポーネントの違い

August 18, 2020
執筆者
Shiori Yamazaki
寄稿者
Twilio の寄稿者によって表明された意見は彼ら自身のものです
レビュー担当者
Diane Phan
Twilion

react-choose-functional-components-jp

この記事はShiori Yamazakiこちら(英語)で執筆した記事を日本語化したものです。

React:関数コンポーネントとクラスコンポーネントの違い

Reactの世界では、Reactコンポーネントの記述方法が2つあります。関数を使用する方法と、クラスを使用する方法です。最近では関数コンポーネントの使用が増えていますが、その理由はなぜでしょうか。

この記事では、関数コンポーネントとクラスコンポーネントの違いについて、それぞれのサンプルコードを使用しながら説明します。この記事を読めば、モダンReactをより深く理解できるようになるでしょう。

JSXのレンダリング

何よりも明確な違いは構文です。それぞれの名前が示すように、関数コンポーネントはJSXを返すプレーンなJavaScript関数で、クラスコンポーネントはReact.Componentを拡張するJavaScriptクラスです。こちらはrenderメソッドを含みます。少し分かりにくいかもしれないので、簡単な例を見てみましょう。

import React from "react";
 
const FunctionalComponent = () => {
 return <h1>Hello, world</h1>;
};

ご覧のとおり、関数コンポーネントはJSXを返す関数です。ES6で導入されたアロー関数について詳しくないという方は、この関数を使用しない以下の例をご覧ください。

import React from "react";

function FunctionalComponent() {
 return <h1>Hello, world</h1>;
}

関数コンポーネントを使用するrenderはこちら(CodePen)

一方、クラスコンポーネントを定義する場合は、React.Componentを拡張するクラスを作成する必要があります。レンダリング対象のJSXは、renderメソッド内で返されます。

import React, { Component } from "react";

class ClassComponent extends Component {
 render() {
   return <h1>Hello, world</h1>;
 }
}

以下は同じ例ですが、非構造化を使用していません。非構造化について詳しくないという方は、こちらから、ES6で導入された非構造化アロー関数の詳細を確認できます。

import React from "react";

class ClassComponent extends React.Component {
 render() {
   return <h1>Hello, world</h1>;
 }
}

クラスコンポーネントを使用するrenderはこちら(CodePen)

propsの受け渡し

propsの受け渡しは分かりにくいかもしれませんが、クラスコンポーネントと関数コンポーネント、それぞれを使用した記述方法を見てみましょう。以下のコードでは、「Shiori」という名前のpropsを受け渡しています。

<Component name="Shiori" />
const FunctionalComponent = ({ name }) => {
 return <h1>Hello, {name}</h1>;
};

関数コンポーネントでは引数としてpropsを渡しています。ここでは非構造化を使用していますが、非構造化を使用せずに記述することもできます。

const FunctionalComponent = (props) => {
 return <h1>Hello, {props.name}</h1>;
};

この例では、名前の代わりにprops.nameを使用する必要があります。

関数コンポーネントを使用するpropsはこちら(CodePen)

class ClassComponent extends React.Component {
  render() {
    const { name } = this.props;
    return <h1>Hello, { name }</h1>;
 }
}

これはクラスであるため、thisを使用してpropsを参照する必要があります。この場合も、クラスベースのコンポーネントを使用しながら、非構造化を使用してprops内でnameを取得できます。

クラスコンポーネントを使用するpropsはこちら(CodePen)

stateの処理

ご存じのように、Reactプロジェクトにおいてはstate変数の処理が不可欠です。最近まで、stateを処理できるのはクラスコンポーネントのみでしたが、React 16.8でReact Hook useStateが導入されてからは、開発者がステートフル関数コンポーネントを記述できるようになりました。フックの詳細については、公式ドキュメントを参照してください。ここでは、0から始まるシンプルなカウンターを作成し、ボタンを1回クリックすると数が1ずつ増えるようにします。

関数コンポーネントでのstate処理

const FunctionalComponent = () => {
 const [count, setCount] = React.useState(0);

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

関数コンポーネントでstate変数を使用するには、初期状態の引数を取るuseStateフックを使用する必要があります。この場合は0クリックから開始するため、カウントの初期状態は0になります。

もちろん、これ以外にもnullstringobjectなど、JavaScriptで使用できるあらゆる初期状態を利用できます。左側では、useStateが現在の状態とそれを更新する関数を返します。このようにして、配列を非構造化しています。配列の2つの要素についてよく分からない場合は、stateとそのセッターだと考えてください。この例では、2つの要素の関係を分かりやすくするために、countsetCountという名前にしています。

関数コンポーネントを使用するstateはこちら(CodePen)

クラスコンポーネントでのstate処理

class ClassComponent extends React.Component {
 constructor(props) {
   super(props);
   this.state = {
     count: 0
   };
 }

 render() {
   return (
     <div>
       <p>count: {this.state.count} times</p>
       <button onClick={() => this.setState({ count: this.state.count + 1 })}>
         Click
       </button>
     </div>
   );
 }

クラスコンポーネントでのstateを処理する場合、概念は同じですが方法が少し異なります。まず、React.Componentコンストラクタの重要性を理解する必要があります。公式ドキュメントでは次のように定義されています。

Reactコンポーネントのコンストラクタは、マウントされる前に呼び出されます。React.Componentサブクラスのコンストラクタを実装する場合は、他の記述よりも前にsuper(props) を呼び出す必要があります。こうしないと、コンストラクタでthis.propsが定義されず、バグになります

基本的に、コンストラクタを実装してsuper(props)の呼び出しを実行しないと、使用したいすべてのstate変数を定義できません。このため、まずコンストラクタを定義しましょう。コンストラクタ内では、stateキーと初期値を使用してstateオブジェクトを作成します。JSX内で、this.state.countを使用してコンストラクタで定義したstateキーの値にアクセスし、カウントを表示します。セッターもほぼ同じで、構文のみが異なります。

また、onClick関数を記述することもできますが、setState関数は、必要に応じてstateの引数であるprops(オプション)を取ることに注意してください。

onClick={() => this.setState((state) => { return { count: state.count + 1 }; }) }

クラスコンポーネントを使用するstateはこちら(CodePen)

ライフサイクルメソッド

最後にライフライクルについて説明します。もう少しですので頑張りましょう。ご存知のとおり、ライフサイクルはレンダリングのタイミングにおいて重要な役割を果たします。クラスコンポーネントから関数コンポーネントに移行するユーザーにとっては、クラスコンポーネントにおけるライフサイクルメソッド(componentDidMount()など)に代わるものが何であるか、よく分からないと思います。この目的に最適なフックがありますので、早速見ていきましょう。

マウント時(componentDidMount)

ライフサイクルメソッドcomponentDidMountは、最初のレンダリングが完了した直後に呼び出します。以前は、最初のレンダリングの前に呼び出すcomponentWillMountというメソッドもありましたが、今では古い方式とみなされており、新バージョンのReactでの使用は推奨されません。

const FunctionalComponent = () => {
 React.useEffect(() => {
   console.log("Hello");
 }, []);
 return <h1>Hello, World</h1>;
};

componentDidMountの代わりに、useEffectフックと[]の2番目の引数を使用します。useStateフックの2番目の引数は、通常は変更されるstateの配列であり、useEffectはそうした変更時にのみ呼び出されます。ただし、この例のような空の配列の場合は、マウント時に1回呼び出されます。これは完全にcomponentDidMountの代わりとして使用できます。

class ClassComponent extends React.Component {
 componentDidMount() {
   console.log("Hello");
 }

 render() {
   return <h1>Hello, World</h1>;
 }
}

ここでの動作も基本的に同じです。componentDidMountは、最初のレンダリング後に1回呼び出されるライフサイクルメソッドです。

マウント解除時(componentWillUnmount)

const FunctionalComponent = () => {
 React.useEffect(() => {
   return () => {
     console.log("Bye");
   };
 }, []);
 return <h1>Bye, World</h1>;
};

便利なことに、useStateフックはマウント解除にも使用できます。ただし、構文が少し異なりますので注意が必要です。それは、マウント解除時に実行される関数をuseEffect関数内で返す必要があるということです。これは、clearInterval関数など、サブスクリプションのクリーンアップが必要な場合に特に便利です。大規模なプロジェクトの場合、他の方法では深刻なメモリリークの問題が生じる可能性があります。useEffectを使用する利点の1つは、マウントとマウント解除の関数を同じ場所で記述できることです。

class ClassComponent extends React.Component {
 componentWillUnmount() {
   console.log("Bye");
 }

 render() {
   return <h1>Bye, World</h1>;
 }
}

関数コンポーネントを使用するlifecycleはこちら(CodePen)

クラスコンポーネントを使用するlifecycleはこちら(CodePen)

まとめ

どちらのコンポーネントにも一長一短がありますが、モダンReactではいずれ関数コンポーネントが主流になるというのが私の結論です。

上記の例から分かるとおり、関数コンポーネントの記述は短くシンプルです。つまり、開発、理解、テストがしやすいということです。クラスコンポーネントではthisが多用されるため混乱が生じやすいですが、関数コンポーネントを使用すれば、このような混乱を回避して、すべてを簡潔にしておくことができます。

また、Reactチームはクラスコンポーネントに置き換わる(さらにはそれを上回る)機能コンポーネント用Reactフックのサポートに力を入れており、利用可能なReactフックが増えていることもポイントです。Reactチームは当初から、不要なチェックやメモリ割り当てをなくして関数コンポーネント内でパフォーマンスを最適化すると述べています。それを裏付けるように、先日、関数コンポーネント用の新しいフックとしてuseStateuseEffectが発表されました。しかし同時に、クラスコンポーネントも廃止されないことが確定しています。Reactチームは、今後発生するケースについては関数コンポーネントとフックの段階的導入を検討しています。つまり、一貫性を確保するために、クラスコンポーネントを利用している既存プロジェクトを関数コンポーネントで完全に書き換える必要はないということです。

Reactには多くの有効なコーディングスタイルがありますが、私は上記の理由から、クラスコンポーネントより関数コンポーネントを使用することを推奨します。この記事が、モダンReactを理解するお役に立てば幸いです。Reactの詳細については、公式ドキュメントをご覧ください。また関数コンポーネントとフックの使用例については、Twilio Videoアプリの構築に関する記事もご覧ください。

Shiori Yamazakiは、Platform Experienceチームのソフトウェアエンジニアリングインターンであり、モダンWebアプリケーション開発に熱心に取り組んでいます。連絡先はsyamazaki [at] twilio.comまたはLinkedInです。