※このブログではサーバー運用、技術の検証等の費用のため広告をいれています。
記事が見づらいなどの問題がありましたらContactからお知らせください。


【React + TypeScript】クラスインスタンスをstateで扱う方法と注意点

react state useState Object.assign() TypeScript

投稿日:2021年5月22日

このエントリーをはてなブックマークに追加
Reactでstateとしてクラスのインスタンスを使用したい場合には少し注意が必要です。この記事では実例を踏まえて詳しく解説しています。

はじめに

この記事について

Reactでクラスインスタンスをstateとして扱う場合、Object.assgin()を使って更新するstateオブジェクトを作成することができます。

Object.assign()でprivateの値の取得って可能?

上の説明を聞いた一部の方は

『え?Object.assign()って列挙可能なプロパティをコピーするんでしょ?もしプロパティがprivateの場合ってコピーできないんじゃない?』

結論から言うと【できます】

たしかに普通のプログラミング言語の場合、上のような考え方ではできないと考えるのが普通でしょう。しかし、TypeScriptにおいては最終的にJavaScriptにトランスパイルされて実行されます。クラスのアクセス修飾子はTypeScriptにのみ存在するもので、JavaScriptには存在しないのです。つまり、TypeScriptでのprivateはトランスパイル後には消えてしまうので問題なくコピーできるというわけですね。

少し気持ち悪さはありますが、できるものはできるのです。

環境について

この記事のコードは以下の環境で確認されました。

  • react version 17.0.2
  • typescript version 4.2.4

実践

プロパティがprivateのクラス

まずはクラスを作成してみましょう。

プロパティはprivateにして値の取得はgetter、setterメソッドを使って取得する構成になっています。

GeoPosition.ts
export class GeoPosition {
  // プロパティはprivate
  private lat: number;
  private lng: number;

  /**
   * コンストラクタで初期化しないと
   * use: strict;にした場合
   */
  constructor() {
    this.lat = 0;
    this.lng = 0;
  }

  /**
   * getter
   * @returns
   */
  public getLat() {
    if (!this.lat) {
      return 0;
    }
    return this.lat;
  }

  /**
   * setter
   * @param lat
   */
  public setLat(lat: number) {
    this.lat = lat;
  }

  /**
   * getter
   * @returns
   */
  public getLng() {
    if (!this.lng) {
      return 0;
    }
    return this.lng;
  }

  /**
   * setter
   * @param lng
   */
  public setLng(lng: number) {
    this.lng = lng;
  }
}

privatenumber型でlatlngがありそれぞれgetLat()getLng()という名前のgetter関数とsetLat()setLng()という名前のsetter関数が作成してあります。

失敗例

まず先に失敗例から解説しておきます。

SampleComponent.tsx
import React, { useState } from "react";
import { GeoPosition } from "./GeoPosition";

const SampleComponent = () => {
  const [geoPosition, setGeoPosition] = useState<GeoPosition>(
    new GeoPosition()
  );

  /**
   * 緯度のinputの値が更新された場合のonclick処理
   * @param event
   */
  const changeLat = (event: React.ChangeEvent<HTMLInputElement>) => {
    const geoPositionClone = Object.assign({}, geoPosition);
    geoPositionClone.setLat(Number(event.target.value));
    setGeoPosition(geoPositionClone);
  };

  /**
   * 経度のinputの値が更新された場合のonclick処理
   * @param event
   */
  const changeLng = (event: React.ChangeEvent<HTMLInputElement>) => {
    const geoPositionClone = Object.assign({}, geoPosition);
    geoPositionClone.setLng(Number(event.target.value));
    setGeoPosition(geoPosition);
  };

  return (
    <div>
      <div>
        ({geoPosition.getLat()},{geoPosition.getLng()})
      </div>
      <input
        placeholder="緯度を入力"
        type="number"
        value={geoPosition.getLat()}
        onChange={changeLat}
      ></input>
      <input
        placeholder="経度を入力"
        type="number"
        value={geoPosition.getLng()}
        onChange={changeLng}
      ></input>
    </div>
  );
};

export default SampleComponent;

このようにすると更新時に以下のようなエラーがはかれてしまいます。

ターミナル
TypeError: geoPositionClone.setLat is not a function

setter、getter関数がなくなってしまってprivateのプロパティとやり取りできなくなってしまいました!

なぜこの様になるのかは下のObject.assign()についての記事で解説しています。

【JavaScript】Object.assign()を使ったオブジェクトコピーに注意

解決策:assign()メソッドにクラスをインスタンス化して渡す

解決方法としてはObject.assign()に渡す第一引数に渡すオブジェクトを更新対象のクラスをnewしたインスタンスにしてあげればOKです!

SampleComponent.tsx
import React, { useState } from "react";
import { GeoPosition } from "./GeoPosition";

const SampleComponent = () => {
  const [geoPosition, setGeoPosition] = useState<GeoPosition>(
    new GeoPosition()
  );

  /**
   * 緯度のinputの値が更新された場合のonclick処理
   * @param event
   */
  const changeLat = (event: React.ChangeEvent<HTMLInputElement>) => {
    const geoPositionClone = Object.assign(new GeoPosition(), geoPosition); // クラスのインスタンスをnewして渡す
    geoPositionClone.setLat(Number(event.target.value));
    setGeoPosition(geoPositionClone);
  };

  /**
   * 経度のinputの値が更新された場合のonclick処理
   * @param event
   */
  const changeLng = (event: React.ChangeEvent<HTMLInputElement>) => {
    const geoPositionClone = Object.assign(new GeoPosition(), geoPosition); // クラスのインスタンスをnewして渡す
    geoPositionClone.setLng(Number(event.target.value));
    setGeoPosition(geoPosition);
  };

  return (
    <div>
      <div>
        ({geoPosition.getLat()},{geoPosition.getLng()})
      </div>
      <input
        placeholder="緯度を入力"
        type="number"
        value={geoPosition.getLat()}
        onChange={changeLat}
      ></input>
      <input
        placeholder="経度を入力"
        type="number"
        value={geoPosition.getLng()}
        onChange={changeLng}
      ></input>
    </div>
  );
};

export default SampleComponent;

今回の場合、GeoPositionuseState<T>()Tの対象のクラスなのでこれを初期化して渡しました。

補足になりますが、値のセットを必ずセッターの関数を介していることにも注意しておきましょう。Object.assign()の引数を使って値をセットしようとしてはいけません。

const changeLat = (event: React.ChangeEvent<HTMLInputElement>) => {
    const geoPositionClone = Object.assign(new GeoPosition(), geoPosition, {
      lat: Number(event.target.value),
    }); // クラスのインスタンスをnewして渡す
    setGeoPosition(geoPositionClone);  // [ Error ] getPositionClone is never type
  };

TypeScriptでは無理やりprivateのプロパティに値をセットしようとすると対象のオブジェクトがneverにすることでエラーを無理やり発生させるのです。

注意点

オブジェクトの場合に比べて、クラスインスタンスではどう頑張っても処理が少し重くなります。
基本的にはstateはできる限りオブジェクトで設定して、クラスのインスタンスは必要になったときに生成してstateオブジェクトから値をわたして上げるのが理想的でしょう。

このエントリーをはてなブックマークに追加


関連記事

記事へのコメント