20190330のReactに関する記事は9件です。

React vs React Static速度対決...したけど、引き分け

結論

React製の超シンプルなWEBページを静的サイトジェネレータ「React Static」で静的ページにしたけど、別に速くならなかった:neutral_face:

https://speed-test-react.netlify.com/
https://speed-test-react-static.netlify.com/

背景

コンポーネント指向の書き方が大好きなので、全然状態の変化しないフツーのサイトもReactで書きたくなりました。
ただ、表示が遅くなりそう...
ただ、誰がいつ見ても同じ内容なのに、一々ブラウザのJSでレンダリングしてもらうなんて...

静的サイトジェネレータを使えば、高速表示のままReactで開発できるのでは...? 

やったこと

画像とテキストが並ぶ超シンプルなSP前提のWEBページをReactとReact Staticで作ってみました。
Ajax通信も一切ありません。

React版

https://speed-test-react.netlify.com/
https://github.com/IYA-UFO/speed-test-react

  • React
  • create-react-app
  • styled-components
  • BootStrap4のCSS
  • 合計700KB程度の画像 一部別サーバー

React Static版

https://speed-test-react-static.netlify.com/
https://github.com/IYA-UFO/speed-test-react-static

上記+React Static

比較ツール

パフォーマンス測定超初心者なので、ググりながらやりました。

Chrome devtools ダウンロード速度10MB CPU4倍遅い設定(MacBook Pro)

比較観点

静的ページ、特にマーケティング系のページにとって、重要なのはファーストビューがちゃんと見えるまでの時間です。
それを比べたい、

...いざ、勝負

結果

Chrome devtools Performanceタブ

比較.png

変わらねぇええ!!
画面左端がindex.htmlを取得した瞬間です。これより前の出来事にフロントエンドは関係ありません。
上の波みたいなのがCPUの使用量を示します。
大雑把に以下です。

  • 青 HTMLパース
  • 黄色 JS実行
  • 紫 CSS適用

よく見ると、React側はレンダリング前に50msほどJSの実行時間があります。これが心配していた「遅延」です。
React Staticは、その賢い仕様上、HTML、CSSの処理が終わった後にイベントなどを処理するJSをマウントしています。ただ、今回は活躍の機会はありません。

Chrome devtools Networkタブ

比較2.jpg

ちょっと...違う..?
今回のページにはファーストビューにヒーローイメージ?(320KBのやつ)があるので、その子の表示が体感速度のカギとなります。
React側はmain.jsによるHTML生成が終わるまでヒーローイメージをリクエストできないのに対し、React Staticは最初からMax6本リクエストを出していると思われます。おかげで700msほど?速く落とせています!

見た目

比較3.gif

変わらねぇええ
主題はファーストビューの高速表示でしたが、複数回やれば逆転するレベルで同じです。
React+styled-componentsが裏でこそこそ何しているのかは知りませんが、React Staticは古き良きHTML⇨CSS追加の表示ですね。

結論

超シンプルなReactアプリケーション?を静的ジェネレータにかけても、表示がすごく速くなることはない。

感想

パフォーマンスの測定

dentoolsの表示の2割も理解していません。ブラウザの動きを深く知らないとキビシイ世界のようです。
devtoolsの見方を体系的に学びたい...

静的サイトジェネレータ

今回のReactさんは50msでHTMLを出しましたが、本格的なReactアプリケーションでは数秒かかることも珍しくないと思います。静的サイトでも本格的に運用すれば数十のコンポーネントを使うわけで、そのとき、静的サイトジェネレータの真の力が唸るのかもしれません。

もちろん、CMSからAjaxでデータを取得する場合、ジェネレータによる事前ビルドの効果は明らかでしょう。

おわりに

ブラウザのことよく知らないのに背伸びしました。仮説、間違いのご指摘、なんでも全力で歓迎いたします。
それまで、Reactでいいや

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Electron & React & Redux & TypeScript アプリ作成ワークショップ をやってみた2

https://qiita.com/y_ohr/items/5c18dd1621b5342a05ea の続き。

概要

以下をやってみた記録。良記事に感謝。

環境

Node.jsとnpmのバージョン
$ node -v
v10.13.0
$ npm -v
6.4.1

component の作成

ユーザー名入力画面の作成

ts/components/UserForm.tsx
import React from 'react';
import IUser from '../states/IUser';
import { TextBox } from './TextBox';

/**
 * ユーザ名を入力して表示する
 */
class UserForm extends React.Component<IUser, {}>{
    public render() {
        return (
            <div>
                <p>
                    <TextBox label="ユーザー名" type="text" value={this.props.name}
                        onChangeText={this.onChangeText} />
                </p>
                <p>名前: {this.props.name}</p>
            </div>
        );
    }

    private onChangeText = (value: string) => {
        // action や store ができてから書く
    }
}

action と action creator の作成

uuidインストール
$ npm install --save uuid && npm install --save-dev @types/uuid
ts/actions/UserNameEvents.ts
import Redux from 'redux';
import { v4 as UUID } from 'uuid';

/**
 * ユーザー名を変更するアクション・タイプ
 */
export const CHANGE_USER_NAME = UUID();

/**
 * ユーザー名を変更するアクション
 */
export interface IChangeUserNameAction extends Redux.Action {
    /** 変更する名前の文字列 */
    name: string;
}

/**
 * ユーザー名変更アクション・クリエイター
 * @param name 変更する名前の文字列
 * @return ユーザー名変更アクション
 */
export const createChangeUserNameAction: Redux.ActionCreator<IChangeUserNameAction> = (name: string) => {
    return {
        name,
        type: CHANGE_USER_NAME,
    };
};

reducer を作成する

cloneインストール
npm install --save clone && npm install --save-dev @types/clone
ts/reducers/UserReducer.ts
import Clone from 'clone';
import Redux from 'redux';

import { CHANGE_USER_NAME, IChangeUserNameAction } from '../actions/UserNameEvents';
import IUser, { initUser } from '../states/IUser';

export const UserReducer: Redux.Reducer<IUser> = (childState = initUser, action) => {
    let newChildState: IUser = childState;
    switch (action.type) {
        case CHANGE_USER_NAME:
            {
                newChildState = Clone(childState);
                newChildState.name = (action as IChangeUserNameAction).name;
            }
            break;
    }
    return newChildState;
};

store を作成する

ts/Store.ts
import { combineReducers, createStore } from 'redux';
import { UserReducer } from './reducers/UserReducer';
import IUser from './states/IUser';

/**
 * store のデータ型を定義する。(親state)
 * 
 * プロパティには、管理する child_state を指定する
 */
export interface IState {
    User: IUser;
    // state が増えたら足していく
}

// 複数の reducer を束ねる
const combinedReducers = combineReducers<IState>({
    User: UserReducer,
    // reducer が増えたら足していく
});

// グローバルオブジェクトとして、store を作成する。
export const store = createStore(combinedReducers);

// improt store from './Store' とアクセスできるように default として定義する
export default store;

store と component を連結させる

ts/components/UserForm.tsx
diff --git a/ts/components/UserForm.tsx b/ts/components/UserForm.tsx
index 36a22f1..6c3f614 100644
--- a/ts/components/UserForm.tsx
+++ b/ts/components/UserForm.tsx
@@ -1,5 +1,7 @@
 import React from 'react';
+import { connect, MapStateToPropsParam } from 'react-redux'; // 追加
 import IUser from '../states/IUser';
+import { IState } from '../Store'; // 追加
 import { TextBox } from './TextBox';

 /**
@@ -22,3 +24,9 @@ class UserForm extends React.Component<IUser, {}>{
         // action や store ができてから書く
     }
 }
+// 追加 -->
+const mapStateToProps = (state: IState) => {
+    return state.User;
+};
+export default connect(mapStateToProps)(UserForm);
+// <- 追加

component から action を reducer に送信する

ts/components/UserForm.tsx
diff --git a/ts/components/UserForm.tsx b/ts/components/UserForm.tsx
index 6c3f614..a51d8f8 100644
--- a/ts/components/UserForm.tsx
+++ b/ts/components/UserForm.tsx
@@ -1,7 +1,8 @@
 import React from 'react';
 import { connect, MapStateToPropsParam } from 'react-redux'; // 追加
 import IUser from '../states/IUser';
-import { IState } from '../Store'; // 追加
+import { createChangeUserNameAction } from '../actions/UserNameEvents'; // 追加
+import store, { IState } from '../Store'; // 変更
 import { TextBox } from './TextBox';

 /**
@@ -21,7 +22,7 @@ class UserForm extends React.Component<IUser, {}>{
     }

     private onChangeText = (value: string) => {
-        // action や store ができてから書く
+        store.dispatch(createChangeUserNameAction(value));
     }
 }
 // 追加 -->

HTMLへのレンダリング

ts/index.tsx
diff --git a/ts/index.tsx b/ts/index.tsx
index c25e4e5..58d6af1 100644
--- a/ts/index.tsx
+++ b/ts/index.tsx
@@ -1,9 +1,15 @@
 import React from 'react';
 import ReactDom from 'react-dom';
+import { Provider } from 'react-redux'; // 追加
+import UserForm from './components/UserForm'; // 追加
+import Store from './Store'; // 追加

 const container = document.getElementById('contents');
-
+// 変更 -->
 ReactDom.render(
-    <p>こんにちは、世界</p>,
+    <Provider store={Store}>
+        <UserForm />
+    </Provider>,
     container,
 );
+// 変更 <--

ビルドして動作確認する。

webpack実行とelectron起動
$ npm run build
$ npm start

動いた!

image.png

資産構成

image.png

感想

  • ちゃんと動いて感動した
  • Reduxに慣れが必要
  • クラスや定数のimportが縦横無尽で追えなくなる。脳内マップが必要。
  • ライブラリに追加したuuidcloneは一般的なベストプラクティスなのだろうか?
  • componentなどのUI関連クラスと、actionなどのRedux関連クラスは、もう少しフォルダ等を整理して、関心を分離できないのだろうか?こういうものなのだろうか。
  • VSCodeでソースを整形しても、tslintに警告されないようにしたい。

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScript: Duplicate identifier 'LibraryManagedAttributes'. 解消法

環境

ts-lint: 5.14.0
yarn: 1.15.2
node: v10.15.1

エラーログ

/Users/ユーザー名/プロジェクト名/node_modules/@types/react-dom/node_modules/@types/react/index.d.ts
(2777,14): Duplicate identifier 'LibraryManagedAttributes'.

原因

調べてみたところ、LibraryManagedAttributesの定義が
①node_modules/@types/react/index.d.ts
②node_modules/@types/react-dom/node_modules/@types/react/index.d.ts
と2箇所存在。
@types/reactの定義がyarn.lockにバージョン違いで2つ定義されていたことが原因。

解消法

①package.jsonに"resolutions"を追加。
②yarn install実施

package.json
  "devDependencies": {
    "@types/react": "^16.8.10",
    "@types/react-dom": "^16.8.3",
    ...
  },
  "resolutions": {
    "@types/react": "^16.8.10"
  }

③yarn.lockを確認してみると
"@types/react@*", "@types/react@^16.8.10":
といった形でバージョンを統一。

yarn.lock
"@types/react-dom@^16.8.3":
  version "16.8.3"
  resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.8.3.tgz#6131b7b6158bc7ed1925a3374b88b7c00481f0cb"
  integrity sha512-HF5hD5YR3z9Mn6kXcW1VKe4AQ04ZlZj1EdLBae61hzQ3eEWWxMgNLUbIxeZp40BnSxqY1eAYLsH9QopQcxzScA==
  dependencies:
    "@types/react" "*"

"@types/react@*", "@types/react@^16.8.10":
  version "16.8.10"
  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.10.tgz#1ccb6fde17f71a62ef055382ec68bdc379d4d8d9"
  integrity sha512-7bUQeZKP4XZH/aB4i7k1i5yuwymDu/hnLMhD9NjVZvQQH7ZUgRN3d6iu8YXzx4sN/tNr0bj8jgguk8hhObzGvA==
  dependencies:
    "@types/prop-types" "*"
    csstype "^2.2.0"
    ...

参考サイト

https://stackoverflow.com/questions/52399839/typescript-duplicate-identifier-librarymanagedattributes

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS S3を使ってホームページを公開する

背景

独自ドメインを取得してホームページを開設したかった。
レンタルサーバを利用しなくてもAWSのS3を利用して簡単にできるようだったので、試しにやってみる。
参考(AWSチュートリアル)

全体像

Untitled Diagram.jpg

作業概要

だいたいの作業手順はこんな感じです。

  1. 独自ドメインの購入
  2. バケット作成
  3. Route53へバケットのエイリアスレコード追加

作業内容

独自ドメインの購入

AWSドキュメントに、より詳細な内容が書かれていますので参考に。AWSドキュメント

  • AWSコンソールにログイン後、サービスからRoute53を選択する。
  • 画面真ん中あたりのテキストボックス(Type a domain name)に取得したいドメイン名を入力し、Checkボタンをクリックする。(.com以外のドメインが欲しい場合は右隣のリストボックスから選択すること)
    image.png

  • Add to cartをクリックし、欲しいドメインをカートに加える。
    (画像はgetsampleapp.comをカートに加えた後のキャプチャです)
    画面を下にスクロールしてcontinueボタンをクリックする。
    image.png

  • ドメインの詳細情報を入力します。
    image.png

  • 購入します。
    以下の作業を行うと、Complete Purchase

    • 真ん中あたりあるチェックボックス「I have read and agree...」 にチェックを入れる
    • 登録したメールアドレスにAWSから確認用メールが届くので、メール内のリンクにアクセスする。 image.png

作成が完了するとroute53のRegistered domainsに出現します。
image.png

バケットの作成

AWSサービスの中からS3を選択し、バケットを作成する。
ホスティングするウェブサイトと同じ名前のバケットを作成すること(AWSチュートリアル参考)。

バケットポリシーを設定する

画面下のポリシージェネレータを利用すると簡単にjsonを生成することができます。
image.png
こんな感じで良いかと思います(ARNは作成したS3バケットのARNを設定してください)
image.png
Generate Policyボタンをクリックし、画面に表示されたポリシーをコピペして貼り付けてください。
※バケットのパブリックアクセス設定内の新規のパブリックバケットポリシーをブロックする (推奨)にチェックが入っているとポリシーの保存ができないので要注意です(そりゃそうか...って感じですが)。
image.png

ウェブサイトのホストとして設定する

AWSチュートリアルを参考に、S3のバケットをウェブサイトのホストとして利用する設定をします。

ひとまず簡単なindex.htmlを作成してホスティングの設定をするのが良いかと思います。

S3エンドポイントへアクセスしてみる

設定がうまくいっていればエンドポイントへアクセスできるはずなのでお試ししてみてください。
AccessDenyエラーが出るときはバケットのアクセス権限周りの設定を再確認してみると良いかもしれません。

Route53へエイリアスレコードの追加

AWSチュートリアルのエイリアスレコードを追加するを参考に、エイリアスレコードを設定します。
Nameと同じ名前でないとAlias Targetとして表示されないので注意です。同じ名前のバケット名を作成したのはそのためか。と一人で納得しました。
image.png

作成後、http://xxxx.com(xxxxは購入したドメイン名)にアクセスすると、いい感じにアクセスできます。

詰まったところ

全体的にあまりつまるところはなかったけど、地味にちょこちょこ引っかかってしまいました...

バケットポリシーが保存できない

S3アクセス権限の「パブリックアクセス設定」 新規のパブリックバケットポリシーをブロックするにチェックが入ってた。チェックを外して保存できた

S3のエンドポイントにアクセスしてもコンテンツが表示されない

S3アクセス権限の「パブリックアクセス設定」 バケットにパブリックポリシーがある場合、パブリックアクセスとクロスアカウントアクセスをブロックする にチェックが入ってた。チェックを外してアクセスできた

エイリアスターゲットにS3のバケットが表示されない

バケット名のスペルが誤っていた。正しい名前(Route53レコード追加画面のNameと同じ名前)のバケット名を作成したらエイリアスターゲットとして選択することができた

今後の課題と感想

ACMとCloudFrontを利用してHTTPS化したい。文書を書くのに途中で力尽きてしまって急に適当になってしまいがちなので、なんとか工夫したい...段々雑になってごめんなさい...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでinput type=fileのフォームを作っていい感じにスタイルする

input要素って、typeがfileになると途端にスタイルが面倒くさくなりますよね。
reactでそれっぽいコンポーネントを作ったので、残しておきます。

import React, { useState } from 'react';
import styled from 'styled-components';

const Wrapper = styled.div`
  display: flex;
`;
const Label = styled.label`
  お好きにどうぞ
`;
const Input = styled.input`
  display: none;
`;
const FileName = styled.p`
  お好きにどうぞ
`;

const onChange = (event, cb, setFileName) => {
  cb(event);
  const targetName = event.target.files.item(0).name;
  setFileName(targetName);
};

const InputFile = props => {
  const [filename, setFileName] = useState('選択されていません');
  if (props.type !== 'file') return <p>typeの指定、間違ってるよ</p>;
  return (
    <Wrapper>
      <Label>
        ファイルを選択
        <Input
          {...props}
          onChange={e => onChange(e, props.onChange, setFileName)}
        />
      </Label>
      <FileName>{filename}</FileName>
    </Wrapper>
  );
};

export default InputFile;

使い方はこんな感じ

<InputFile
  type='file'
  accept='image/*'
  onChange={e => なんか好きな処理}
/>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactと他のライブラリとのインテグレーション

参考:他のライブラリとのインテグレーション

公式サイトではjQueryプラグイン(Chosen)のラッパーのサンプルコードが掲載されていますが
実際に要素を追加したときの動きの違いを確認しました。

componentDidUpdateにあるChosenライブライのtriggerを呼び出さないと、
Reactがstateの変化を検知してrenderを実行してもChosenが生成するドロップダウンのリストには反映されません。

App.js
import React, { Component } from "react";
import "./App.css";
import $ from "jquery";
import "chosen-js/chosen.css";
import "chosen-js/chosen.jquery.js";

class Chosen extends React.Component {
  componentDidMount() {
    this.$el = $(this.el);
    this.$el.chosen();

    this.handleChange = this.handleChange.bind(this);
    this.$el.on("change", this.handleChange);
  }

  componentDidUpdate(prevProps) {
    if (prevProps.children !== this.props.children) {
      this.$el.trigger("chosen:updated"); //ここをコメントアウトすると・・・
    }
  }

  componentWillUnmount() {
    this.$el.off("change", this.handleChange);
    this.$el.chosen("destroy");
  }

  handleChange(e) {
    this.props.onChange(e.target.value);
  }

  render() {
    return (
      <div>
        <select className="Chosen-select" ref={el => (this.el = el)}>
          {this.props.children}
        </select>
      </div>
    );
  }
}

class App extends Component {
  constructor(prpps) {
    super(prpps);

    this.state = {
      items: ["one", "two", "three"]
    };
  }

  handleAddList = () => {
    this.setState({ items: this.state.items.concat(["four"]) });
  };

  render() {
    return (
      <div className="App">
        <button onClick={this.handleAddList}>追加</button>

        {/* Reactの守備範囲外 */}
        <Chosen onChange={value => console.log(value)}>
          {this.state.items.map(item => (
            <option key={}>{item}</option>
          ))}
        </Chosen>

        {/* Reactの守備範囲 */}
        <select>
          {this.state.items.map(item => (
            <option key={item}>{item}</option>
          ))}
        </select>
      </div>
    );
  }
}

export default App;

componentDidUpdateをコメントアウトした場合、追加ボタンによって新たな要素が追加され
DOMには4つめの要素が生成されますが、Chosenが生成しているSelectには表示されません。
image.png

もちろん、普通のを利用しているリストには追加した要素が表示されます。
image.png

基本的にはReactのComponentとして提供されているライブラリを利用する方がよいですが、
必要に迫られた場合には、やれないことはないということですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React.jsの環境構築

Reactの環境構築は10〜15分あれば出来るので思ったより簡単でした。

node.jsのインストール

公式サイト node.js
注 Homebrewがインストールされている程で進めていきます。
brew install node
バージョンの切り替えツールのインストール
npm install -g n
バージョンのインストール
n 8.12.0
   ↑
ここでError: sudo requiredというエラーが出た場合
sudo mkdir /usr/local/nでディレクトリを作り再びTRY!
自分の場合正しいのか少し不安ですがsudo n 8.12.0で進めてしまいましたがそれでも出来ました。

ディレクトリにroot権限を与える
sudo chown -R $(whoami) /usr/local/n
インストールとバージョン確認
n 8.12.0
node -v

JSのパッケージマネージャyarnのインストール

brew install yarn --without-node
ここでError: invalid option: --without-nodeというエラーが出た場合
brew install yarn --ignore-dependenciesとしてTRY!
インストールの確認
yarn --vesion
バージョンが確認できればOKです。

webpack4インストール

webpackとライブラリなどのインストール
yarn add --dev webpack webpack-cli

これでReact.jsの開発環境が整いました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Formik で使う yup のスキーマパターン

はじめに

以前に別の記事に書いたのですが、うちの職場ではフォームの管理は Formik、バリデーションチェックは yup を使っています。

いくつかのやりたいことは無理やり実現してる感がありますが、使っていて便利だなーと思うのでいくつかのパターンをシェアします。
GitHub の issue も漁りましたが会心の解決策に出会えてないものもあり…、もっと良いパターンがあったらぜひとも知りたい。(切望

Formik の利用パターン

私の環境で使用してる Formik の使い方は以下の通りです。

const FormView: React.SFC<Props> = ({ ...props }) => {
  const schema = genSchema(something);

  return 
    <Formik
      initialValues={initialValues}
      validationSchema={schema.validations}
      onSubmit={(values, { setSubmitting }) => {
        // something...
        setSubmitting(false);
      }}
      render={(props: FormikProps<Values>) => (
        <>
          <CustomField item={schema.somethingSchema} />

          <Button
            title="Submit"
            onPress={props.handleSubmit}
            disabled={props.isSubmitting}
          />
        </>
      )}
    />
  );
}

Formik には validationvalidationSchema の使い方があると思います。
ここでは、 yup で定義したスキーマを直接渡す形を取っています。
(そのせいで(?)、詰まった部分もあります。。。)

ポイントは、 genSchema という処理を作って、そこでスキーマを取得していることです。(詳細は後述)

yup のスキーマ

getSchema

getSchema は以下のような形で定義します。
引数 arg を渡しているのは、このほうがダイナミックなスキーマの利用ができるからです。

export const genSchema = (arg: any) => {
  const somethingSchema = {
    fieldValidations: Yup.string().required(),
    // something...
  };

  const validations = Yup.object().shape({
    something: somethingSchema.fieldValidations,
    // others...
  });

  return {
    somethingSchema,
    // others...
    validations,
  };
}

スキーマのパターン

いくつかの(よく使う)スキーマパターンを紹介します。
なお、 .email.required みたいなベーシックなものは省略します。

公式ドキュメントの内容を自分で試してみて、コメント(感想?)を載せているようなものですので、どうぞ温かい目で見守ってください。

mixed.oneOf(...)

ラジオボタンや yes / no のチェックなど、複数の中から一つを選択させるフォームで活用します。
選択肢のリストを変数として渡せば、何かのはずみで別の値が渡されたとしてもエラーとして弾いてくれます。

yup.ref と組み合わせて、パスワード(確認用)のフォームを作ることも簡単です。
.requiredを忘れると、選択しなくても通るので注意してください。

const radioButtonSchema = {
  fieldValidations: Yup.string().oneOf(optionsList).required(),
  // ...
}

const passwordSchema = {
  fieldValidations: Yup.string().required(),
  // ...
}
const confirmPasswordSchema = {
  fieldValidations: Yup.string().oneOf([Yup.ref('password')]).required(),
  // ...
};

mixed.when

公式に以下のような記載があります。

Adjust the schema based on a sibling or sibling children fields.

別のスキーマ(おそらく同じ Yup.object().shape({ ... }) で定義されたもの)を参照することができます。
自分自身の値を参照しようとすると、循環参照エラー(?)になりました。

活用シーンは、 yes / no ボタンを用意し、 no を選択したケースだと必須入力の詳細内容フォームが現れる、といったケースです。

const detailSchema = {
  fieldValidations: Yup.string().when('yesNoCheck',
    (value, schema) => value === 'no'
    ? schema.required()
    : schema,
  // ...
}

このパターンでは、他の yesNoCheck スキーマの値を参照し、No の場合は必須入力に、Yes の場合はパスさせる、という処理をしています。
detailSchema の初期値が null になる場合は、 schema.nullable() にしてあげる必要があります。

mixed.test

.test は第三引数で渡すテスト関数の返り値で検証します。

All tests must provide a name, an error message and a validation function that must return true or false or a ValidationError. To make a test async return a promise that resolves true or false or a ValidationError.

テスト関数は true / false / ValidationError を返す必要があります。
また、async も使えるみたいです。

.test では、自分自身の値と、thisが使えるのが特徴です。
前回の記事にも書きましたが、 arrow 関数を使ってしまうと this が束縛されてしまうので、 function(value) { ... } の形で定義する必要があります。

Note that to use the this context the test function must be a function expression (function test(value) {}), not an arrow function, since arrow functions have lexical context.

動的なハンドリング

この記事を書こうと決意した理由がこの動的なハンドリングです。
私が求めていた動的なハンドリングが発生するケースとは、フォームの値以外の条件(たとえばバックエンドから返ってきたステータスなど)によって、フォームの表示 / 非表示が変化する場合です。

ここでは、 genSchema の引数を利用します。

export const genSchema = (arg: any) => {
  const somethingSchema = {
    fieldValidations: arg === 'something'
    ? Yup.string().required()
    : Yup.string(),
    // something...
  };
  // ...
}

この方法を使うことで、フォーム以外の値でもフォームのバリデーションをコントロールすることができます。

ちょこっとコラム(その1)

動的なハンドリングについて、当初 .whencontext を利用できないかと画策しました。参考になりそうな issue も見つけました。

.whencontext で渡したパラメータを $something で参照できる機能を持ちます。
この質問者のケースでは、 Formcontext props を渡すことで実現できたとあります。

ですが私の場合、(validationSchema を使っているからか?) JSX に context を渡してもうまくハンドルできませんでした。

schema.isValidschema.validation を直接コールするとバリデーションチェックはできているものの、Formik を介してのチェックが走りません。
そのため、やむ終えず上記の手法にチェンジしました。

ちょこっとコラム(その2)

Formik には <Form /><Field /> が用意されています。
<Form /><form><Field /><input> のラッパーとして定義されているので、React Native でそのままは利用できません。

そのため CustomField Component を用意することで、フォームの入力フィールドを作りました。
TextInput 以外にも、ラジオボタンなども定義可能です。

const CustomField: React.SFC<Props> = ({ item }) => (
  <Field name={item.fieldName}>
    {({ form, field }: FieldProps) => {
      const hasError = !!form.errors[field.name] && !!form.touched[field.name];

      return (
        <View style={{ ... }}>
          <TextInput
            style={{ ... }}
            value={field.value}
            placeholder={item.defaultLabel}
            onChangeText={(v) => form.setFieldValue(field.name, v)}
            onBlur={() => form.setFieldTouched(field.name)}
          />
          <View style={{ ... }}>
            {hasError && (
              <Text style={{ ... }}>
                {form.errors[field.name]}
              </Text>
            )}
          </View>
        </View>
      );
    }}
  </Field>
);

おわりに

.whencontext はハンドリングしようとしてドツボにはまりました。(結局、解決策を見つけられなかった)

同僚に相談したら、
「フィールドの表示制御も以ってるんでしょ?それなら genSchema に引数渡しちゃいなよ。」
って言われて解決しました。
視野を狭めず、いろんな可能性を考えることの重要性を再認識させられました。

同じ悩みを持つ誰かの助けになれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Electron & React & Redux & TypeScript アプリ作成ワークショップ をやってみた1

概要

以下をやってみた記録。良記事に感謝。

環境

Node.jsとnpmのバージョン
$ node -v
v10.13.0
$ npm -v
6.4.1

npmプロジェクトの作成

フォルダとpackage.json作成
$ mkdir electron-react-app
$ cd electron-react-app
$ npm init
package.json
{
  "name": "electron-react-app",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

gitで作業を記録することにする。
以降、記事には記載しないが、適時コミットしてワークショップを進める。

gitの初期設定
$ git init
$ git add .
$ git commit -m "npm init"
$ git remote add origin (your remote repo)
$ git push -u origin master
日本語対策
$ set LANG=ja_JP.UTF-8
.gitignore
node_modules/
/dist/

必要なライブラリをインストールする

各ライブラリの詳細は、元記事参照。

各種ライブラリインストール
$ npm install --save react react-dom redux react-redux styled-components
$ npm install --save-dev electron typescript tslint webpack webpack-cli ts-loader tslint-loader
$ npm install --save-dev @types/react @types/react-dom @types/redux @types/react-redux

ここから二日目の内容です。

TypeScript コンパイラ・オプションファイルの作成

tsconfig.json作成
$ "./node_modules/.bin/tsc" --init
tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "jsx": "react",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "sourceRoot": "./tsx",
    "inlineSourceMap": true,
    "inlineSources": true
  },
  "include": [
    "./ts/**/*"
  ]
}

tslint 設定ファイル の作成

tslint.json作成
$ "./node_modules/.bin/tslint.cmd" --init
tslint.json
{
    "defaultSeverity": "error",
    "extends": [
        "tslint:recommended"
    ],
    "jsRules": {},
    "rules": {
        "quotemark": [
            true,
            "single",
            "jsx-double"
        ]
    },
    "rulesDirectory": []
}

webpack.config.js の作成

2か所タイポがあったのは秘密(以下は修正済み)。

webpack.config.js
const path = require('path');

module.exports = {
    // node.js で動作することを指定する
    target: 'node',
    // 起点となるファイル
    entry: './ts/index.tsx',
    // webpack watch したときに差分ビルドができる
    cache: true,
    // development は、source map fileを作成。再ビルド時間の短縮などの設定となる
    mode: 'development', // "production" | "development" | "none"
    // ソースマップのタイプ
    devtool: 'source-map',
    // 出力先設定 __dirname は node ではカレントディレクトリのパスが格納される変数
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'index.js'
    },
    // ファイルタイプ毎の処理を記述する
    module: {
        rules: [{
            // 正規表現で指定する
            // 拡張子 .ts または .tsx の場合
            test: /\.tsx?$/,
            // ローダーの指定
            // TypeScript をコンパイルする
            use: 'ts-loader'
        }, {
            // 拡張子 .ts または .tsx の場合
            test: /\.tsx?$/,
            // 事前処理
            enforce: 'pre',
            // TypeScript をコードチェックする
            loader: 'tslint-loader',
            // 定義ファイル
            options: {
                configFile: './tslint.json',
                // airbnb という JavaScript スタイルガイドに従うには下記が必要
                typeCheck: true,
            },
        }],
    },
    // 処理対象のファイルを記載する
    resolve: {
        extensions: [
            '.ts',
            '.tsx',
            '.js', // node_modules のライブラリ読み込みに必要
        ]
    },
};

HTML の作成

index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Electronチュートリアル</title>
</head>

<body>
    <div id="contents"></div>
    <script src="dist/index.js"></script>
</body>

</html>

main.js の作成

main.js
const {
    app,
    BrowserWindow
} = require('electron')

// レンダープロセスとなるブラウザ・ウィンドウのオブジェクト。
// オブジェクトが破棄されると、プロセスも終了するので、グローバルオブジェクトとする。
let win

function createWindow() {
    // ブラウザウィンドウの作成
    win = new BrowserWindow({
        width: 800,
        height: 600
    })
    // index.htmlをロードする
    win.loadFile('index.html')
    // 起動オプションに、"--debug"があれば開発者ツールを起動する
    if (process.argv.find((arg) => arg === '--debug')) {
        win.webContents.openDevTools()
    }
    // ブラウザウィンドウを閉じたときのイベントハンドラ
    win.on('closed', () => {
        // 閉じたウィンドウオブジェクトにはアクセスできない
        win = null
    })
}

// このメソッドは、Electronが初期化を終了し、
// ブラウザウィンドウを作成する準備ができたら呼び出される。
// 一部のAPIは、このイベントが発生した後にのみ使用できる。 
app.on('ready', createWindow)

// 全てのウィンドウオブジェクトが閉じたときのイベントハンドラ
app.on('window-all-closed', () => {
    // macOSでは、アプリケーションとそのメニューバーがCmd + Qで
    // 明示的に終了するまでアクティブになるのが一般的なため、
    // メインプロセスは終了させない
    if (process.platform !== 'darwin') {
        app.quit()
    }
});

app.on('activate', () => {
    // MacOSでは、ドックアイコンがクリックされ、
    // 他のウィンドウが開いていないときに、アプリケーションでウィンドウを
    // 再作成するのが一般的です。
    if (win === null) {
        createWindow()
    }
});

コンパイル確認用スクリプトの記述

ts/index.tsx
import React from 'react';
import ReactDom from 'react-dom';

const container = document.getElementById('contents');

ReactDom.render(
    <p>こんにちは、世界</p>,
    container,
);

コンパイルの確認

webpack実行とelectron起動
$ "./node_modules/.bin/webpack"
$ "./node_modules/.bin/electron" ./

npm script を利用する

package.json
diff --git a/package.json b/package.json
index a98d36d..f69de75 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,8 @@
   "description": "",
   "main": "main.js",
   "scripts": {
+    "build": "webpack",
+    "start": "electron ./",
     "test": "echo \"Error: no test specified\" && exit 1"
   },
   "author": "",
webpack実行とelectron起動
$ npm run build
$ npm start

ここから三日目の内容です。

child_state の作成

ts/IUser.ts
export default interface IUser {
    name: string;
}

export const initUser: IUser = {
    name: '',
};

component の作成

ラベル付きテキストボックスの作成

ts/components/TextBox.tsx
import React from 'react';

// 親コンポーネントから渡されるプロパティを定義する
interface IProps {
    // ラベル文字列
    label: string;
    // テキストボックスのタイプ
    type: 'text' | 'password';
    // テキストボックスに表示する値
    value: string;
    // 値の確定時にその値を親プロパティが取得するためにコールバック関数を提供する
    onChangeText: (value: string) => void;
}

export class TextBox extends React.Component<IProps, {}>{
    // DOMエレメントをレンダリングする
    public render() {
        // ラベルが設定されていない場合は、label を出力しない
        const label = (!!this.props.label) ?
            <label>{this.props.label}</label> :
            null;
        return (
            <span>
                {label}
                <input name="username" type={this.props.type} value={this.props.value}
                    onChange={this.onChangeText}></input>
            </span>
        );
    }

    // 値を変更したら、store.dispatch で action を reducer に渡して、state を更新する。
    // state が更新されたら component の prop が更新され、再レンダリングされ、テキストボックスの内容が変更される。
    private onChangeText = (e: React.ChangeEvent<HTMLInputElement>) => {
        this.props.onChangeText(e.target.value);
    }
}

続きは次回。

感想

  • Electronはもちろんだけど、周辺ツールについても、とても勉強になる
  • TypeScriptの書き方も同上

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む