C++ ときどき ごはん、わりとてぃーぶれいく☆

USAGI.NETWORKのなかのひとのブログ。主にC++。

React で TypeScript な Component 内の一部のメソッドから他の定義済みのはずのプロパティーや state へのアクセスが undefined になった時に思い出したいメモ

期待動作する例

↓は button をポチると hidden で置いてあるファイルアップロード用の input の click を発火するコンポーネント的な例です。

import React, { Component } from "react";

interface IProps { }
interface IState { }

export default class FileUploader extends Component<IProps, IState> {
  // ↓ TypeScript で refs 的なことをしたい場合に増える仕込み
  private fileUploader:React.RefObject<HTMLInputElement>;

  constructor(props:IProps)
  {
   super(props);
   // ↓ ref る仕込み
   this.fileUploader = React.createRef<HTMLInputElement>();
   this.state = { my_awesome_value: undefined };
  }

  // ↓こう書くと this.fileUploader など期待動作する
  //   あえて method{} ではなく property = closure な定義方法をしているところが核心です
  handleClick = (): void => {
    // 無事 ref れていれば input 要素の click を発火できてファイルオープンダイアログが出てくれるところ
    this.fileUploader.current?.click();
    // note: この handleClick の定義方法の場合は state へのアクセスも期待動作します
  }

  render() {
    // ↓ ref= に仕込み
    return (
      <div>
        <button onClick={this.handleClick} />
        <input type="file" id="file" ref={this.fileUploader} style={{ display: "none" }} />
      </div>
    );
  }
}

ダメな例

  // ↓メソッドの定義方法を一見シンプルなただのよくあるメソッド的に記述すると…
  //   handleClick 自体は button の onClick から呼ばれてくれますが…
  handleClick() {
    // ↑このメソッドの定義方法だと↓でfileUploader が undefined で runtime に死ぬ
    this.fileUploader.current?.click();
    // note: この handleClick の定義方法の場合は state へのアクセスも undefined で runtime に死にます
  }

React/TypeScript初心者にはしばらく何が起きているのか脳内が?で満たされながらの状況調査となりましたがわかってしまえば JavaScript 界隈ではしばしば遭遇する類のトラブルだったようです。

参考

wasm-bindgen で fetch する rustwasm 公式の example の async で JsFuture な run が実行時に失敗した場合にも安全に対応できるようにする方法のメモ

問題

  • この example の runrustwasm/wasm-bindgen のような、実装上GitHub APIで取得可能なポジトリーが実在する場合は問題ありません。
  • リポジトリーが存在しないパターンを run に与えて fetch させると async/JsFuture 処理系の都合か二度と run を使用できない .wasm が生成されてしまいます。
    • 実際にやってみると、一度非実在リポジトリーを叩いてエラーを出すと実在のリポジトリーを叩こうと叩くまいと run は二度と動作しなくなります。

問題は example の run の最後の2行:

    // Use serde to parse the JSON into a struct.
    let branch_info: Branch = json.into_serde().unwrap();

    // Send the `Branch` struct back to JS as an `Object`.
    Ok(JsValue::from_serde(&branch_info).unwrap())

解決方法

matchjsonErr<serde_json::error::Error>Err<JsValue> に射る:

 // 解決方法①段階
 match json.into_serde() as Result<Branch, serde_json::error::Error> {
 Ok(branch_info) => Ok(JsValue::from_serde(&branch_info).unwrap()),
 Err(e) => Err(JsValue::from_str(&format!("{}", e))),
 }

↑だとまだ json.into_serde()Ok だけど JsValue::from_serdeErr だった場合は死んでしまいますが、 GitHub API は実在しないリポジトリーに対しても JSON を返してはくれるので、さしあたりは問題にはなりません。そこも match すると↓:

 // 解決方法②段階
 match json.into_serde() as Result<Branch, serde_json::error::Error> {
  Ok(branch_info) => match JsValue::from_serde(&branch_info) {
   Ok(jsvalue) => Ok(jsvalue),
   Err(e) => Err(JsValue::from_str(&format!("{}", e))),
  },
  Err(e) => Err(JsValue::from_str(&format!("{}", e))),
 }

↑のそこはかとない多段 match のダサさを Resultmapmap_err で整理すると:

 // 解決方法③段階
 (json.into_serde() as Result<Branch, serde_json::error::Error>)
  .map(|branch_info| JsValue::from_serde(&branch_info).unwrap())
  .map_err(|e| JsValue::from_str(&format!("{}", e)))
  • (1. json.into_serde()): json.into_serde() := Ok<Branch>Err<serde_json::error::Error> です
  • (2. map): (a) が Ok の場合に mapfrom_serde の結果を unwrap := Ok<JsValue> または Err<serde_json::error::Error> です
  • (3. map_err): (a) または (b) の何れかが Err<serde_json::error::Error> の場合は map_errErr<JsValue> に変換されます

こうすると run の内部で Err<serde_json::error::Error> が発生しても Uncaught (in promise) missing filed `name` at line 1 column 104 のような console.error が吐かれるだけで、 run が使用不能には至らなくなります。

この他にも unwrap() により panic が発生する可能性がある部分はありますが、 GitHub が 404 になったり JSON レスポンスを廃止しない限りは事実上死には至らないです。学習用途ではなく実用する場合はより厳密に async/JsFuture の絡む内部で panic が発生しないように気にする必要はあります。

諸事情により React で TypeScript な web アプリの中から window に global な変数を write access する方法のメモ

Window をオレオレ派生した interface をでっちあげてキャストすれば window の任意のプロパティーに write access できる。後は野となれ山となれ。

↓例、React の state 持ちの Component な App のインスタンスwindow.app な global 変数として登録:

App.tsx:

// 凡例 ☆ := 本質的な部分
// 凡例 ◎ := おまけ, state を扱う部分

// ☆Window を派生して app 変数を持ったインターフェースをでっちあげる
interface IGlobalWindow extends Window{ app?: App; }

// ◎ これらは constructor で state を定義したい場合に必要なインターフェース
interface IProps {}
interface IState { aaa?: any; }

export default class App extends Component<IProps, IState> {

 constructor( props: IProps ) {
  super(props);
  // ◎ ctor ではトリアエズ {} な aaa を this: App の .state 放り込んでおいてみます
  this.state = { aaa: {} }
  // ☆ window をオレオレ派生したインターフェースに明示的にキャストした上で .app 変数に書き込み; このメモの本質的な部分です
  (window as IGlobalWindow).app = this;
 }

 componentDidMount()
 {
  // ◎ 適当なタイミングで App の state を設定してみます
  this.setState( { aaa: 123 } );
 }

}

↑こうすると、ウェブブラウザーの inspector ツール的なそれの Console からも↓のように App のインスタンスやそのプロパティーにアクセスできるようになります:

> app.state.aaa
< 123

おまけメモ: React Developer Tools

これをブラウザー側へ入れられる場合は、導入後に React のページを読み込むと $r で React の Root にぶちこんだ Appインスタンスにアクセスできるようになります。今回のメモの主題とは異なりますが、外から中を見たいだけなら素直に React Developer Tools を入れればよいです。

f:id:USAGI-WRP:20200729151138p:plain

React プロジェクトの src 内のコンポーネントから諸事情によりウェブサイト外部の SCRIPT を読む方法のメモ

諸事情の例

4つの方法

もっとあるというのはさておき。

1. react-script-tag

この方法では DOM 構造的には in-situ な感じで <script> がぶちこまれます。

準備:

# .jsx 向けしか存在しないので .tsx では @ts-ignore して使います
yarn add react-script-tag

実用:

import React, { Component } from "react";
// note: ↓ .tsx ならこのコメントディレクティブを付けないとエラーになります。 .jsx なら不要です
// @ts-ignore
import ScriptTag from "react-script-tag";

export default class MyAwesomeMainCanvas extends Component {
  render() {
    return (
      <div>
        <canvas id="main-canvas"></canvas>
        <ScriptTag src="runner.main-canvas.something.js" />
      </div>
    );
  }
}

2. react-helmet

この方法では DOM 構造的には <head><script> (というか実際には任意の何かしら、 <title> とか <link> でも) をぶちこみます。

注意:

  • この方法を採用すると依存ライブラリーの都合か strict mode で警告↓がでて不穏になります。<script> 用途だけが目的の場合は他の方法がよいかもしれません。 ( Issue -> https://github.com/nfl/react-helmet/issues/426 )
index.js:1 Warning: Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://fb.me/react-unsafe-component-lifecycles for details.
* Move code with side effects to componentDidMount, and set initial state in the constructor.
Please update the following components: SideEffect(NullComponent)

準備:

# for .jsx
yarn add react-helmet
# for .tsx; 両方必要になります
yarn add react-helmet @types/react-helmet

実用:

import React, { Component } from "react";
// for .jsx & .tsx
import { Helmet } from "react-helmet";

export default class MyAwesomeMainCanvas extends Component {
  render() {
    return (
      <div>
        <canvas id="main-canvas"></canvas>
        <Helmet>
          <script src="runner.main-canvas.something.js"></script>
          <title>にゃーん/title>
        </Helmet>
      </div>
    );
  }
}

3. DOM 操作でぶちこむ

いわゆるごりごり手書きした的な方法。

import React, { Component } from "react";

export default class MyAwesomeMainCanvas extends Component {
  render() {
    return (
      <div>
        <canvas id="main-canvas"></canvas>
      </div>
    );
  }
  
  componentDidMount() {
    const s = document.createElement( "script" );
    s.src = "runner.main-canvas.something.js";
    s.async = true;
    document.body.appendChild( s );
  }
}

複数箇所で使用したいとか再利用性が欲しい場合は方法(1),(2)を選択するほかか、この方法(3)も手書きオレオレコンポーネント化する方法もあります。コンポーネント化の方法はこのメモの本論ではないので省略。

4. React の Effect Hook 機能

方法(3) のごりごり手書きを若干変態気味に実装する亜種のような方法です。

カスタム・フックを作って:

import { useEffect } from 'react';

export default const myHook =
  (src) =>
  {
    useEffect(
      () =>
      {
        const s = document.createElement( "script" );
        s.src = src;
        s.async = true;
        document.body.appendChild( s );
        return () => {
          document.body.removeChild( s );
        };
      }
    , [src]
    );
  };

カスタム・フックをひっかける:

// for .jsx
// import myHook from 'myHook.jsx';
// for .tsx
import myHook from 'myHook';

const Demo = props => {
  importScript("runner.main-canvas.something.js");
}

参考

React なプロジェクトを TypeScript に移行したら3つの困りが発生したけど解決できたメモ

こまったこと:

1. カスタムソースディレクトリー ./src.reacttsc が認識できなかった

諸事情により ./src ではなく ./src.react に react 用のソースを配置していたが、 tsconfig.jsoninclude では "." 文字の入ったディレクトリーは認識しない仕様らしく困りました。

考えた解決方法:

  1. symlink を作って渡す
  2. ./src-react に変える

もんやり感はありましたが (2) でディレクトリーの名前から "." を排除して解決しました。ちなみに React のソースファイルパスのカスタムは react-app-rewired で eject-free に対応しています。

2. .tsx 化したモジュールで他の .tsx 化したモジュールを import できなかった

  • import Hoge from "./Hoge.tsx" ← ダメ🙅‍♀️
  • import Hoge from "./Hoge" ← ヨシ!🙆‍♀️

3. 独自定義のタグが JSX.IntrinsicElements に無いですよエラーで困った

独自定義のタグ、例えば GUI のルック・アンド・フィール的に Xel を使っていると <x-box> とか <x-button> とか使います。TypeScript化していない状態では一般的なHTMLタグと同様に埋め込めましたが、 TypeScript にしたら

Property 'x-box' does not exist on type 'JSX.IntrinsicElements'. TS2339

などと怒られてビルドできなくなりました。

単純なタグとしてのみの場合、かつ独自定義のタグが少ない場合は、

declare global
{ namespace JSX
 { interface IntrinsicElements
  { "my-awesome-tag": React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>; 
  }
 }
}

↑のようにまじめに TypeScript らしさを残したハック的なコードを <my-awesome-tag> を使いたい .tsx ファイル冒頭へ加えればよい。よいのだけど、 Xel を使いたい場合のようにたくさんのタグをぶちこみたい場合や、独自定義のタグに独自定義のプロパティーも使いたい場合は、 TypeScript にこだわって疲弊するよりは↓型は any にして "my-awesome-tag": any; のようにすると少し楽です。

参考

Web 向けの3D の GPU 描画コンテキストお取り扱いライブラリーの2020-07時点のメモ: ES 系, Unity, Unreal Engine, Rust 系

note: 2D 系(eg. phaser, ggez, etc. )は今回のメモでは含めていません。

Lang Library License WebGPU WebGL native ES & wasm
ES babylon.js Apache-2.0 Ready 2 %a n/a pure ES
ES three.js (+A-Frame) MIT(+MIT) (unknown†1) 1 + (2) n/a pure ES
ES playcanvas MIT (unknown†2) 2 %b n/a pure ES
C++ Unreal Engine *1 (unknown) 1 %c D3D12, Metal, OpenGL wasm(Emscripten)
C# Unity *2 (unknown) 2 %d D3D12, Metal, OpenGL wasm(Emscripten)
Rust unrust Apache-2.0 (unknown) 2 OpenGL wasm
Rust Amethyst(≈rendygfx-hal) Apache-2.0 or MIT (unknown; potentially maybe) (2;potentially) Vulkan, Metal, OpenGL wasm(; experimental)
Rust harmony Zlib (unknown†4; potentially yes) (2;potentially) Vulkan, Metal wasm(; help wanted)
Rust oxidator MIT (potentially yes) n/a Vulkan, D3D12 wasm(; not yet)
Rust piston MIT (unknown) n/a n/a n/a

おまけ: 所感

  • pure-ES 系で特に WebGPU を見据える場合は babylon.js で書くのが良さそう
  • リッチなエンジン系は…
    • Unreal EngineHTML5 出力は現実的に考えない方がいいです(UE5移行へWebGPUと併せて期待したいけど、期待しないでおいた方がいいかも)
    • 豊富なエコシステムやコミュニティーもあるので Unity で WebGL2 バックエンドにするのが現時点だけを見るなら現実的で良いかもしれません
    • Web 親和性の点では playcanvas もよいかもしれません
  • Rust 系は...
    • unrust は現時点で Web へきっちり出力できるたぶん唯一の3D-GPU描画系です
    • Amethyst は wasm+GL サポートを開発途中ですが、このメモの作成時点では全ての要素技術にかなり強い技師でも辛うじてバギーながらChromeで動くような動かないような微妙な出力に辿り着ける程度の状態です
    • harmony は描画バックエンドのポテンシャル的には WebGPU 対応、 OpenGL 対応により Web 出力できますが、採用する ECS バックエンドやマルチスレッドに最適化しまくっている事から WebWorker の制限と性能の懸念などもあり今の所 Author は Web 出力にそれほど積極的ではないようです。(Webよりエンジンとしての機能実装を進めたい状況でもあります)
    • oxidatorRTS ゲームエンジンに特化していますが、 Author も WebGPU での Web 出力は視野にあるようです。いまのところは WebGPU や rust と wasm 処理系の進化、ウェブブラウザーの進化待ちのようです
    • piston は Web に興味無さそうです

少なくとも、 Web 向けアプリの 3D-GPU 描画系に現時点で Rust を採用するのはわりとしんどいです。3年後に WebGPU と Rust/wasm 処理系が最高の選択肢になっている可能性はわりと高いとは思いますが、いまはまだしんどいです。 Rust を採用したい場合も、恐らく 3D-GPU 描画系はうまく機能分離し、できるだけ疎結合に近い設計にしつつ、ビジネスロジック部分は Rust で書いて wasm-unknown-unknown ターゲットで wasm を wasm-bindgen で library としてフロントエンドから叩く感じで作っておき、フロントエンドとして GUI系、 3D-GPU 描画系は pure-Web 系またはそれに近い要素技術でカバーするのが現時点では合理的かもしれません。

例えばフロントエンドは React あるいは yew と HTML と ES のライブラリーで GUI を作り、 3D-GPU 描画系は babylon.jsbabylon.rs で取り込み、アプリ本体の処理、状態やリソースの管理、Entity-Component-System、などなどは pure-Rust または wasm 対応が面倒ではない範囲の Rust のエコシステムを用いて作り、 wasm-bindgen で .wasm に固めるとか。

Arch Linux 環境で cargo が言うことを聞かなくなったメモ

症状

  • 数日ぶり程度に使用した Arch Linux 環境の rust 処理系で
  • cargo+nightly を受け付けてくれなくなって困った ( -Z したかった )
  • rustup toolchain では stable, nightly が installed 状態

原因

  • rust は rustup を per user で導入して使っていた"はず"だったのに
  • /sbin/cargo が実行されていた ( which cargo で判明 )
  • /sbin/cargo は stable

なぜ /sbin/cargo が環境に "いつのまにか" install されてしまったのか考えると、この数日の間に実験的に yay で rust 製のツールをシステムへ install した事を思い出しました。 yay での install ではビルドツールチェインも芋蔓し、途中の選択肢によってはビルドツールチェインはビルド後に remove できたりするのですが、おそらく yay で rust 製のツールをシステム導入した際にビルドツールチェインの remove を選択ミスしたためシステムパッケージ版の rust が導入された状態になってしまい、意図しない cargo = /sbin/cargo~/.cargo/bin/cargo よりパスが優先される状態に陥っていたのだろうと思います。

解決

  • yay または pacman で rust パッケージを削除 ( per user で導入している rustup には影響しません )

おまけ: Arch Linux ゆえ

同様の症状の状態は Ubuntu など他の GNU/Linux 環境でも発生させる事はできますが、 AUR と yay のようにユーザーの環境でソースコードからビルドするタイプのパッケージ管理システムを常用していない限り、"意図せず"に発症する事は無いと思います。Gentoo で emerge している場合は似た状況がうっかり発生する事もあるのかも。