投稿日:2021年5月22日
Reactでstateとしてクラスのインスタンスを使用したい場合には少し注意が必要です。この記事では実例を踏まえて詳しく解説しています。
Reactでクラスインスタンスをstateとして扱う場合、Object.assgin()を使って更新するstateオブジェクトを作成することができます。
上の説明を聞いた一部の方は
『え?Object.assign()って列挙可能なプロパティをコピーするんでしょ?もしプロパティがprivateの場合ってコピーできないんじゃない?』
結論から言うと【できます】
たしかに普通のプログラミング言語の場合、上のような考え方ではできないと考えるのが普通でしょう。しかし、TypeScriptにおいては最終的にJavaScriptにトランスパイルされて実行されます。クラスのアクセス修飾子はTypeScriptにのみ存在するもので、JavaScriptには存在しないのです。つまり、TypeScriptでのprivateはトランスパイル後には消えてしまうので問題なくコピーできるというわけですね。
少し気持ち悪さはありますが、できるものはできるのです。
この記事のコードは以下の環境で確認されました。
まずはクラスを作成してみましょう。
プロパティはprivateにして値の取得はgetter、setterメソッドを使って取得する構成になっています。
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;
}
}
privateはnumber型でlat、lngがありそれぞれgetLat()、getLng()という名前のgetter関数とsetLat()、setLng()という名前のsetter関数が作成してあります。
まず先に失敗例から解説しておきます。
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()についての記事で解説しています。
解決方法としてはObject.assign()に渡す第一引数に渡すオブジェクトを更新対象のクラスをnewしたインスタンスにしてあげればOKです!
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;
今回の場合、GeoPositionがuseState<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オブジェクトから値をわたして上げるのが理想的でしょう。