Open24

クラスコンポーネントの基本とJSのクラスを理解する

Yug (やぐ)Yug (やぐ)

へえ、クラスコンポーネント時代はrender() {/*render中の処理*/}と言う感じでわざわざrender()メソッドっていうの定義してその中に処理を書く必要があったのね。んでマウント時の処理はconstructor()メソッド内に書くと。

てことはアンマウント時はさっきのcomponentWillUnmount()ってクラス作ってその中にアウマウント時の処理書いて、みたいな感じか?他のライフサイクルメソッドも同じなんじゃないかな

Yug (やぐ)Yug (やぐ)

なるほど、やっぱそうっぽい。まぁアンマウント時というかアンマウント直前と言うべきか。

useEffectの最後のクリーンアップ処理と同じ意味だろうな。

タイマーの無効化やネットワークリクエストのキャンセルなど、メソッドで必要なクリーンアップを実行します。

Yug (やぐ)Yug (やぐ)

フックが正式番になったのはバージョン16.8.0でリリースは2019年2月6日。5年以上前か。
https://qiita.com/uhyo/items/246fb1f30acfeb7699da

なので関数コンポーネントが正式版になったとかそういうことではない。

関数コンポーネントは最初からあったのだが、stateやライフサイクルメソッドを持てないためStateless Functional Component(SFC)と呼ばれ、用途が限定されていた。

v16以前は下記のようにSFCとClassComponentを使い分けていた

  • 状態(state)を持つ必要がないコンポーネント:SFC
  • 状態(state)を保つ必要があるコンポーネント:ClassComponent

https://qiita.com/tomomo51/items/6dff84b0d1ef6bc58bba

Yug (やぐ)Yug (やぐ)

そこにフック(関数コンポーネントから使える新しいAPI)の登場で、関数コンポーネントを使った方が楽だからこっち使おうぜ!になったのね

Yug (やぐ)Yug (やぐ)

つまりクラスコンポーネントはもともとstateの管理ができたので、わざわざフックを導入する必要はなかった。フックを導入したのは関数コンポーネントの方。

なのでクラスコンポーネントでフックは使えない。

Q. じゃあどうやってsetState的なことを以前はやっていたんだろう?
A. this.setStateって書き方で実現してた。説明を見る限り今のuseStateのsetterと全く同じっぽい。書き方が違うだけか
https://ja.react.dev/reference/react/Component#setstate

Yug (やぐ)Yug (やぐ)

stateの宣言自体はconstructor()メソッド内でやる。

でconstructor()の引数にpropsがわたってきて、それをsuper(props)とすることでthisが使えるようになる(?)
https://ja.react.dev/reference/react/Component#constructor

クラスをよく知らないのでsuper()がわからん。調べる
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/super
https://zenn.dev/nekulala/articles/112cbae4626a40

なるほど

  1. super()を使うと親のメソッドつまり親のconstructor()を呼び出せる
  2. 子クラスが呼び出された時に設定した引数が子クラスのconstructorの引数とsuperの引数に入る
    • でもそのsuperに渡した子の引数は親の初期化ロジックによって操作されるよ、ていう感じ
  3. super()を呼び出してからじゃないとthis.〇〇のthisは使えない
    ->追記: 勘違いだった。別に呼ばなくても使えた

3つ目だけ違和感あるなぁ。
別にsuper()で親呼び出さなくてもthisって書いたら自分自身(子)を認識できるというのが直感的だが、それができないのか...。
「親を呼び出さないと自分すら認識できない」ってことか、変な仕様だなぁ...。

「super()をすることで親の初期化ロジックを子でも再利用する」のはまぁ何となく理解できる。が、
「super()をすることで自分が誰かを認識する」のは直感に反する...。

継承した時点で自分が継承者(=子)であることは確定してるから自分が誰かはわかってるはずでは?

いやでもそんなこと言ったら親の初期化ロジックも自動で継承すりゃあええやん、ていう話になっちゃうな。

しかし事実はそうなっていない。子は、親の「初期化ロジック」も「自分自身が誰か」も認識できないのがデフォルトになってる。

Q. ...じゃあそういう仕様にしたのはなぜ?(Claudeに聞いてみる)
A. 特定の引数だけ親に渡す、つまり他の引数(プロパティ)は省略することで、子独自の初期化ロジックでカスタマイズできたりするから。つまり制約が強すぎると柔軟性が無くなるから。

class Child extends Parent {
  constructor(name, age) {
    // 親クラスに特定の引数だけを渡したい
    super(name);
    this.age = age;
  }
}

なるほどなぁ、確かにそうだわ。
もしすべての初期化ロジックを自動で親から全部受け取ってしまう仕様だったら、
「このプロパティだけはそのロジックいらなくて全然違う方法でカスタマイズしたいんだけどな...」みたいになったときに、その親のロジックが邪魔してきちゃうからか。なるほど


あ、前提からすごい勘違いしてた。super()を呼び出さないとthisが使えないというのはあくまでconstructor内のみの話で、super()もconstructor()もやらずとも子の普通のメソッド内でthis使えた(子が返ってきた)し親の初期化ロジックもそのまま使われるようになってた。

てことはsuper()の存在意義は「初期化ロジックをカスタマイズしたい人はsuper使ってね」ていうあくまでオプション的な立ち位置なんだろうな。

厳密に言うと、
「superの引数を調整することでどの変数に対して親の初期化ロジックを再利用するか明示的に表現できる。一応親の初期化ロジックに渡した子の引数を無理やり独自ロジックで上書きしちゃえばsuper()のことを考慮せずに独自ロジック適用することは可能っちゃ可能ではある。が、「この変数は親のロジック使います」「この変数は独自ロジックにします」というのをわかりやすく表現するためにsuper()はある。つまりsuperは制約ではなくもっと緩い、表現をしやすくするもの、単にわかりやすくするものという理解をした。

class Animal {
    constructor (name, age) {
        this.name = name + name + name;
        this.age = age * 100;
    }

    speak() {
        console.log(`親: ${this.name}${this.age}歳です`);
    }
}

class Dog extends Animal {
    constructor (name, age) {
        super(name);  // 親の初期化ロジックを再利用するのはnameに対してだけです、とここで「表現」できる
        this.age = age * 2;  // 子の独自ロジック
    }

    // ちなスプレッド演算子で引数を一気に渡すこともできる
    // constructor (...args) {
    //     super(...args);
    // }

    speak() {
        console.log(`子: ${this.name}${this.age}歳です`);
    }
}

const parent = new Animal('harry', 1);
const son = new Dog('john', 2);
parent.speak();  // 親: harryharryharryは100歳です
son.speak();  // 子: johnjohnjohnは4歳です

Q. なので究極的にはsuper無くてもいいんじゃね?説(Claudeに聞いてみる)
A. そうでもないっぽい。super()がなくては複雑になってしまう、または実現できない可能性がある、みたいなシーンがある。

/* こういう親クラスがあったとする */
class Animal {
  speak() {
    return "Some sound";
  }
}
/* super()を使うことで実現できるケース */
class Dog extends Animal {
  speak() {
    return super.speak() + " Woof!";  // 親クラスの機能を活かしつつ、拡張する
  }
}

/* super()を使わないと複雑になってしまうケース */
class Dog extends Animal {
  speak() {
    // 親クラスのメソッドを直接参照することはできない
    // this.__proto__.speak() や Object.getPrototypeOf(this).speak() などは、thisのコンテキストが変わってしまうため、正しく動作しない可能性がある
    return Animal.prototype.speak.call(this) + " Woof!";  // 一応ちゃんと機能はする
  }
}

ほぉ、確かに、なるほど、て感じ。

ちなsuper classが親クラス、sub classが子クラスのことで、super classの初期化ロジックを使用することができるのでsuper()という名前になっている。

とりあえずsuper()は理解

Yug (やぐ)Yug (やぐ)

別にクラスコンポーネント時代でもconstructor()使わないでよかったっぽい...?
クラスコンポーネントのトップレベルに普通に書いちゃうのでもOKではあるらしい

モダンな JavaScript 構文を使用している場合、コンストラクタはほとんど必要ありません。代わりに上記のコードは、モダンブラウザや Babel などのツールでサポートされているパブリッククラスフィールド構文を使用して書き直すことができます。

https://ja.react.dev/reference/react/Component#constructor

代替として、JavaScriptのクラスフィールド構文(Class Fields宣言)という機能によって直接プロパティ(state)を初期化できる = letとかconstとかが要らない

// クラスフィールドを使用する場合
class Example extends React.Component {
  state = {
    count: 0
  };
}
// constructorを使用する場合
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
}
Yug (やぐ)Yug (やぐ)

Q. じゃあconstructor()は何のためにあんねん

  • 古いJavaScriptでも動作する
  • propsを使った初期化が明示的
  • 古いブラウザではBabelなどのトランスパイラが必要になっちゃうというクラスフィールドのデメリットが無い

みたいな感じっぽい

とはいえconstructorはあんま使われてなかったらしい

constructor()
Mounting時
JSXおよびTSXのフォーマットではまず見かけません。JavaScriptでReactを書く場合にstateの初期化やactionのバインドのために使用します。

https://qiita.com/yuria-n/items/3c3fc8d29fd2e56ed7a9

constructor()理解

Yug (やぐ)Yug (やぐ)

bindってなんだ?
https://qiita.com/3062_in_zamud/items/bdea7987fdc1c6d5128c#stateを更新する

class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'Tanaka',
      age: 19
    };
    // ここでメソッドに対してbindをしておく(理由は後に記述)
    this.changeAge = this.changeAge.bind(this);
  }

  // 年齢を+1するメソッドを定義
  changeAge() {
    this.setState(prevState => ({
      // prevStateには前回のstateの値が格納されている
      age: prevState.age + 1
    }));
  }

  render() {
    return (
      <div>
        <h1>Hello! {name}! I'm {age} years old!</h1>
        <button onClick={this.changeAge}>Change</button> {// ボタンを押すとメソッドが呼び出される}
      </div>
    );
  }
}
Yug (やぐ)Yug (やぐ)

↓よりもっと具体的に知りたい

bindしなければいけない理由
深掘りすると色んな要素が絡み合って少し難しいため、簡単にいうと、イベントハンドラに設定したメソッドのなかで記述したthisが、意図したものとは違うものを指してしまうので、bindを使うことでそれを回避している。

Yug (やぐ)Yug (やぐ)

Claudeに聞いたらなるほどーになった

bindが必要な理由は、JavaScriptにおけるthisの挙動に関係しています。以下の3つのポイントで説明します:
1. thisのスコープの問題

  • JavaScriptでは、メソッドが呼び出される際のthisの値は、そのメソッドがどのように呼び出されたかによって変わります
  • イベントハンドラとして関数を渡す場合(例:onClick={this.changeAge})、関数は「別のコンテキスト」で実行されるため、thisの参照が失われてしまいます

2. bindを使用しない場合の問題

// bindしない場合:
<button onClick={this.changeAge}>Change</button>

この場合、changeAgeメソッド内のthisはundefinedとなり、this.setStateを呼び出そうとするとエラーになります。

3. bindによる解決

constructor(props) {
  super(props);
  this.changeAge = this.changeAge.bind(this);
}

bindすることで、changeAgeメソッド内のthisが常にコンポーネントインスタンスを参照するように固定されます。

代替的な解決方法:
最近のReactでは、以下の方法でbindを避けることができます:

  1. アロー関数を使用したメソッドの定義:
changeAge = () => {
  this.setState(prevState => ({
    age: prevState.age + 1
  }));
}
  1. イベントハンドラでアロー関数を使用:
<button onClick={() => this.changeAge()}>Change</button>

ただし、パフォーマンスの観点から、constructorでのbindか、クラスフィールドとしてのアロー関数の定義が推奨されます。

Yug (やぐ)Yug (やぐ)

わかりやすい。

イベントハンドラはクラスコンポーネント内ではなくて別のコンテキストで実行されるからthisの指す先もクラスじゃなくなっちゃう(undefinedになっちゃう)
->だからbind使うことでthisの指す先をクラスコンポーネントとしてbindする(縛る)ようにしよう

てことか

んじゃあこの1行を言語化するなら、

this.changeAge = this.changeAge.bind(this);

「this.changeAgeにthis(クラスコンポーネント)を参照するようにbindしたもの = 右辺」を
「this.changeAgeとして宣言しておきます = 左辺」
ということやね

ちなみに、「イベントハンドラの宣言時」もしくは「(onClickなどの)属性への設定時」にアロー関数を使うならbindしないでも大丈夫だよーていうオプションも一応ある感じね

bind()理解

Yug (やぐ)Yug (やぐ)

「this.changeAgeとして宣言しておきます = 左辺」
のところだけど、それをもっと細かく言うなら、

「thisの中のchangeAgeを探しにいく」ということをイベントハンドラがやっちゃうと、そのthisは違うコンテキストになっちゃうから問題が起きるので、このようにthis.changeAge =とやってしまうことでもはや「thisの中のchangeAge」という意味合いよりかはもはや「this.changeAgeというもの」としてそれを上書きしちゃうみたいなことをしてるので、それによってイベントハンドラなどの外部コンテキストからthis.changeAgeを呼び出しても決められたものを指すことができるみたいなイメージかも

Yug (やぐ)Yug (やぐ)

んーでもなんか納得いかないのは、イベントハンドラ内でthisって使った時点でイベントハンドラを指しちゃうはずで、だから絶対コンテキストは維持できないはずじゃないか...?なぜ維持できる?

this.changeAgeをbindするより「イベントハンドラ内で使用するthisそのもの」をうまくbindするみたいなまったく新しい方法を使う必要があるはずじゃないか?直感的には

それをせずthis.changeAgeとやっても勝手にthisがイベントハンドラから外の世界でbindされてるthisをちゃんと指すようになってるのが不思議だなぁ

だってthisをbindした訳じゃなくてthis.changeAgeをbindしてるだけだもんなぁ、不思議だ

Yug (やぐ)Yug (やぐ)

へぇ、そういうルールなんだ

render()メソッドは唯一、クラスコンポーネントで必ず定義しなければならないメソッドです。

https://qiita.com/3062_in_zamud/items/bdea7987fdc1c6d5128c#render

あ、たしかにrender()メソッド内でjsxをreturnしてる

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

しかも

render()メソッドはpropsやstateが更新されるたびに呼び出される(「純粋」である)ため、この中で直接propsやstateを操作してはいけません。

ということなので、render()内って関数コンポーネントのトップレベルと同じっぽいな

Yug (やぐ)Yug (やぐ)

この「余分なrenderを引き起こす」ってどういう意味だ?

componentDidMount()メソッドは、DOMノードを必要とする初期化などを行います。
データをfetchしたり、タイマーやイベントリスナをセットする時などに利用します。
このメソッド内でsetState()を呼び出す(直接のDOM操作)のは余分なrenderを引き起こすので注意しましょう。

componentDidUpdate()メソッドは、更新が行われた直後に呼び出されます。コンポーネントが更新後、DOMを操作する機会にこのメソッドを使用しましょう。
第一引数に1つ前のprops、第二引数に1つ前のstate、第三引数にsnapshotが入ってきます。(使用しない引数は省略可能です)
こちらのメソッドも、setState()を呼び出す(直接のDOM操作)のは余分なrenderを引き起こすので注意しましょう。

https://qiita.com/3062_in_zamud/items/bdea7987fdc1c6d5128c#componentdidmount

Yug (やぐ)Yug (やぐ)

commitフェーズって実際に画面更新するんちゃうの?このメソッドはcommitフェーズに実行されるはずなのに、UIに表示されないってどゆこと?

componentDidMount()

Mounting時
1度目のrenderが呼ばれた後に1度だけ呼ばれるメソッドです。この時点ではまだUIに表示されていません。

↓の記事の内容だとcommitは実際のUIを更新しているっぽいから言ってることが違うような...

このタイミングで画面上には「world」の文字だけが表示されるようになります

https://zenn.dev/ktmouk/articles/68fefedb5fcbdc#7.-「commitフェーズ」でdomの更新を実施

てかcommitとペイントの違いがわからないことに気付いたのでそれは別スクラップで追うか
https://zenn.dev/yg_kita/scraps/640bd644fbe834

Yug (やぐ)Yug (やぐ)

いや、むしろ逆に、そのメソッドの直前に既にDOM更新(画面更新)してるのでそのメソッドでは画面更新しませんよ、てこと?

...んんんん??DOM作成?renderフェーズでFiberツリー(仮想DOMツリー)作られるのはわかるけど、commitフェーズのcomponentDidMount()が「DOMを作成する」ってどういうことだ???

このメソッドからはDOMが作成されていますが、

Yug (やぐ)Yug (やぐ)

ていうかpropsとstateの比較がよくされてるんだけどこの2つってそもそも明確に区別できなくないか?stateがpropsとして渡されることは多いのでその場合この2つはほぼ同じやんという感覚があり混乱している。
→stringをそのまま渡すとかstateから計算されたものを渡すとかがpropsではあるから同じではなかった
https://ja.legacy.reactjs.org/docs/react-component.html#shouldcomponentupdate

そもそもpropsを受け取ってないコンポーネントはどうなるんだ?