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

メディアサイトでServerlessな構成でSubscriptionモデルを構築する。

メディアサイトの会員管理、Subscriptionモデルをスクラッチで開発する。

  • ユーザー管理、認証は、「Auth0 + カスタムデータベース dynamoDB」か、AWSのCognito。
  • フロントは、React ReduxでSPA。
  • 決済はStripe、StripeのReact Elements。
  • APIはlambdaで、Node.js。

Severless Frameworkを使って、フロントエンド、APIを同時にデプロイ。

Frontend, BackendともJavascriptなので共通なライブラリも作れるはず。

image.png

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

【2019/03| React入門①】ES2015で「Hello World」を出すまで

やること

「ES2015でHello Worldを出す」
WebpackやBabelを使って、ローカルにwebserverを立ててブラウザ上にHTMLを表示する

参考

最短で学ぶReactとReduxの基礎から実践まで/Udemy

ES2015で「Hello worldを出す」までにやったこと

  • エディタ(ATOM)のインストール
  • プロジェクトの作成
    • mkdir project-name
    • cd projet-name
      • git init
    • eho 'project-name' > README.md
      • git status
      • git add .
    • git commit -m "initial commit"
  • ES2015に必要なpackageをinstall
  • yarn initーpackage jsonを作成)
    • yarn add ***
      • package.jsonに必要なパッケージをインストール&直接記述)
    • webpack(@3.3.0)
    • webpack-dev-server(@2.5.1)
      • WebServerをローカルで動かすライブラリ)
    • babel-core(@6.25.0)
    • babel-loader(@7.1.1)
    • babel-preset-react(@6.24.1)
    • babel-preset-es2015(@6.24.1)
  • webpack.config.jsの作成
  • webpack-dev-serverの起動
    • ./node_modules/.bin/webpack-dev-server
      • ブラウザでlocalhost:8080「Hello World」が表示される

ソースコード

index.html
<!DOCTYPE html>
<html lang="" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>udemy_react</title>
  </head>
  <body>
    <div class="container">
      Hello udemy
    </div>
    <script src="bundle.js" charset="utf-8"></script>
  </body>
</html>

package.json
{
    "name": "udemy_react",
    "version": "1.0.0",
    "main": "index.js",
    "author": "*******<mail>",
    "license": "MIT",
    "dependencies": {
        "babel-core": "6.25.0",
        "babel-loader": "7.1.1",
        "babel-preset-es2015": "6.24.1",
        "babel-preset-react": "6.24.1",
        "babel-preset-react-es2015": "^1.3.0",
        "webpack": "3.3.0",
        "webpack-dev-server": "2.5.1"
    }
}
webpack.config.js
var publidDir = __dirname + '/public';
module.exports = {
    entry: [
        './src/index.js'
    ],
    output: {
    path: publidDir,
    publicPath: '/',
    filename: 'bundle.js'
    },
    module] {
      loaders: [{
        exclude: /node_modules/,
        loader: 'babel-loader',
        query: {
            presents: ['react', 'es2015']
        }
           }]
    },
    resolve: {
        extensions: ['_js', '_jsx']
    },
    devServer: {
        historyApiFallback: true,
        contentBase: publidDrir
    }
};

エラーやハマったこと

【問題①】Atomに表示されないフォルダがある

[対策]
Windows版
* [File]-[setting]
* [Packages] treeviewで検索
* [tree-view]のSetting
* [Settings]-[Hide VCS ignored Files]のチェックを外す

[参考]
Atomのツリーに表示されないフォルダやファイルがある?

【問題②】PowerShell スクリプトのエラー「このシステムはスクリプトの実行が~」

[原因]
shellの権限設定

[対策]
shellの実行ポリシー設定を変更する
* コマンド画面で下記コマンドを実行

Set-ExecutionPolicy -Scope CurrentUser RemoteSigned

参考:PowerShell スクリプトの実行が無効となっている場合の対処法

  • 【問題③】webpack起動時エラー >The CLI moved into a separate package: webpack-cli please install 'webpack-cli' in addition to webpack itself to use the CLI -> When using npm: npm i -D webpack-cli -> When using yarn: yarn add -D webpack-cli internal/modules/cjs/loader.js:613 throw err; Error: Cannot find module 'webpack-cli/bin/config-yargs'

[原因]
webpackのバージョンによってwebpack.config.jsの記述方法が異なる模様(同様エラーの記事が大量に出る)

[対処法]
* yarnでインストールするパッケージのバージョンを変更
* Udemyの公開段階で使用されているバージョンに合わせる

自分用メモ

①Atomの機能(htmlテンプレート文)

AtomのHTMLテンプレート
// Atomエディタでhtmlファイルを作成
// 本文に[html]と打ってからtabを押すと下記のテンプレートが自動表示される

<!DOCTYPE html>
<html lang="" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
  </body>
</html>

②.gitignoreー対象がコミットされなくなる

.gitignore
node_modules
// gitignoreの本文に名前を指定したフォルダはコミットされなくなる。
// node_modulesディレクトリがグレーになる


終わり

以上です

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

React-DnDでドラッグ&ドロップを行う(モバイル対応も)

こんにちは、ミツバです。
今回はReact-DnDを利用して、ドラッグ&ドロップを用いたリストの並び替えを実現したいと思います。

react-dnd

ソースコードは以下に置いています。

mitubaEX/react-dnd-example

動作イメージ

drag

テスト環境

テストを行った際の各ライブラリのバージョンを明記しておきます。

  • react: ^15.6.2
  • react-dnd: ^2.6.0
  • react-dnd-html5-backend: ^2.6.0
  • react-dnd-touch-backend: ^0.4.0
  • react-dom: ^15.6.2
  • react-scripts: 2.1.5

setup

create-react-appを用いてサクッと作成します。

npx create-react-app react-dnd-example
cd react-dnd-example

# install
npm install --save react-dnd@2.6.0
npm install --save react-dnd-html5-backend@2.6.0
npm install --save react-dnd-touch-backend@0.4.0

package.jsonの各バージョンは適宜変更してください。

App.jsを変更する

import React, { Component } from 'react';
import SortableList from './SortableList.js';

class App extends Component {
  render() {
    return (
      <div className="App">
        <SortableList />
      </div>
    );
  }
}

export default App;

SortableListを作成する

import React, { Component } from 'react';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import TouchBackend from 'react-dnd-touch-backend';
import DragItem from './DragItem';
import CustomDragLayer from './CustomDragLayer';

class SortableList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      items: [
        {id: 1, name: "aaa"},
        {id: 2, name: "bbb"},
        {id: 3, name: "ccc"}
      ],
    }
  }

  onDrop = (toId, fromId) => {
    const items = this.state.items.slice();
    const toIndex = items.findIndex(i => i.id === toId);
    const fromIndex = items.findIndex(i => i.id === fromId);
    const toItem = items[toIndex];
    const fromItem = items[fromIndex];
    items[toIndex] = fromItem;
    items[fromIndex] = toItem;
    this.setState({items})
  }

  render () {
    return (
      <div>
        {(isAndroid() || isIOS()) && <CustomDragLayer/>}
        {
          this.state.items.map(item => {
            return (
              <DragItem
                key={item.id}
                id={item.id}
                onDrop={this.onDrop.bind(this)}
                name={item.name}
              />
            )
        })
  }
</div>
    )
}
}

function isAndroid() {
  return !!window.navigator.userAgent.match(/Android/);
}

function isIOS() {
  return !!window.navigator.userAgent.match(/iPhone|iPad|iPod/);
}

export default DragDropContext((isAndroid() || isIOS()) ? TouchBackend : HTML5Backend)(SortableList);

いろいろ概念が登場していますが、CustomDragLayerDragItemをrenderしています。
そして最後にDragDropContextに分岐を書いてモバイルの場合とそうでない場合のBackendを切り替えています。
このBackendは、今回HTML5とTouchの2種類ありデバイスによって、Backendを変更することでそのデバイスに対応します。

DragItem, CustomDragLayerを作成する

DragItemは以下のような感じです。

import React, { Component } from 'react';
import { DragSource, DropTarget } from 'react-dnd'

// ドラッグされるSourceの動作を定義する
const dragSource = DragSource("item", {
  beginDrag(props) {
    return props;
  }
}, (connect, monitor) => {
  return {
    connectDragSource: connect.dragSource(),
    connectDragPreview: connect.dragPreview(),
    isDragging: monitor.isDragging()
  };
})

// ドロップされるTargetの動作を定義する
const dropTarget = DropTarget("item", {
  drop(dropProps, monitor) {
    const dragProps = monitor.getItem(); 
    if (dropProps.id !== dragProps.id) {
      dragProps.onDrop(dragProps.id, dropProps.id);
    }
  }
}, connect => {
  return {
    connectDropTarget: connect.dropTarget()
  };
})

class DragItem extends Component {
  constructor(props) {
    super(props)
  }

  getItemStyles() {
    const { isDragging } = this.props

    return {
      opacity: isDragging ? 0.4 : 1,
    }
  }

  render() {
    return this.props.connectDragSource(
      this.props.connectDropTarget(
        <div style={this.getItemStyles()}>
          {this.props.name}
        </div>
      )
    )
  }
}

export default dragSource(dropTarget(DragItem));

ドラッグされるSourceとドロップされるTargetの動作を定義し、最後にComponentを包んでやる流れです。
今回はドラッグされるものもドロップされるものも同じなので、同じComponent(DragItem)を包んでいます。

これでだいたいオッケーなのですが、モバイルだとドラッグした時の軌跡が表示されません。
そのためにDragLayerを導入し、ドラッグの軌跡をそいつに描画してもらいます。

動作イメージ

no_layer

DragLayerを利用して、包んだComponentであるCustomDragLayerは以下のような感じ。

import * as React from 'react'
import { DragLayer } from 'react-dnd'

const layerStyles = {
  position: 'fixed',
  pointerEvents: 'none',
  top: 0,
  left: 0,
  width: '100%',
  height: '100%',
}

function collect(monitor) {
  return {
    item: monitor.getItem(),
    itemType: monitor.getItemType(),
    initialOffset: monitor.getInitialSourceClientOffset(),
    currentOffset: monitor.getSourceClientOffset(),
    isDragging: monitor.isDragging()
  }
}

class CustomDragLayer extends React.Component {
  getItemStyles(currentOffset) {
    if (!currentOffset) {
      return {
        display: 'none'
      }
    }

    // move position
    const x = currentOffset.x
    const y = currentOffset.y
    const transform = `translate(${x}px, ${y}px) scale(1.05)`

    return {
      WebkitTransform: transform,
      transform: transform,
    }
  }

  render() {
    const { item, itemType, isDragging, currentOffset } = this.props

    if (!isDragging) {
      return null
    }

    // render
    if (itemType === 'item') {
      return (
        <div style={layerStyles}>
          <div style={this.getItemStyles(currentOffset)}>
            {item.name}
          </div>
        </div>
      )
    }
    return null;
  }
}

export default DragLayer(collect)(CustomDragLayer)

これでモバイルでもドラッグの軌跡が表示されたと思います。

細かい対応

DragLayerをwebの方にも適応する

今回、DragLayerをモバイルのドラッグ軌跡の描画に用いましたが、ドラッグしたときのアニメーションを個別で決めたいときにも利用できます。
しかし、デフォルトだとwebのドラッグの軌跡は表示されているので、軌跡が二重に表示されます。
なので、それを防ぐために以下のissueのようにドラッグされているときは、軌跡を描画しないようにします。

connectDragPreview() component is also displayed as regular component

componentDidMount() {
  // Preview表示する時のみ、元のComponentの描画を消す
  if (this.props.connectDragPreview) {
    this.props.connectDragPreview(getEmptyImage(), {
      captureDraggingState: true,
    });
  }
}

hoverを利用して入れ替え処理を行う

DropでItemを変えるよりもより直感的に入れ替える方法として、hoverというものがあります。
それを紹介します。

DragItemを以下のように変更します。
今回はdropをhoverに変更し、そこに処理を書いていきました。
他にもいろいろAPIが提供されているので調べてみてください。

import React, { Component } from 'react';
import {
  DragSource,
  DropTarget,
} from 'react-dnd'

const dragSource = DragSource("item", {
  // 変更部分
  beginDrag(props) {
    return {
      id: props.id,
      name: props.name,
      originalIndex: props.findItem(props.id)
    };
  },

  endDrag(props: CardProps, monitor: DragSourceMonitor) {
    const { id, originalIndex } = monitor.getItem()
    const didDrop = monitor.didDrop()

    if (!didDrop) {
      props.onDrop(id, originalIndex)
    }
  },
}, (connect, monitor) => {
  return {
    connectDragSource: connect.dragSource(),
    connectDragPreview: connect.dragPreview(),
    isDragging: monitor.isDragging()
  };
})

const dropTarget = DropTarget("item", {
  // 変更部分
  canDrop() {
    return false
  },

  hover(props: CardProps, monitor: DropTargetMonitor) {
    const draggedId = monitor.getItem().id
    const overId = props.id

    if (draggedId !== overId) {
      const overIndex = props.findItem(overId)
      props.onDrop(draggedId, overIndex)
    }
  },

}, connect => {
  return {
    connectDropTarget: connect.dropTarget()
  };
})

class DragItem extends Component {
  constructor(props) {
    super(props)
  }

  getItemStyles() {
    const { isDragging } = this.props

    return {
      opacity: isDragging ? 0 : 1,
    }
  }

  render() {
    return this.props.connectDragSource(
      this.props.connectDropTarget(
        <div style={this.getItemStyles()}>
          {this.props.name}
        </div>
      )
    )
  }
}

export default dragSource(dropTarget(DragItem));

そしてfindItemは以下のような定義です。
これをDragItemのpropsとして渡します。

findItem = (index) => {
  return this.state.items.filter(c => c.id === index)[0].id
}

動作イメージ

hover

まとめ

react-dndについて調べることがあったので、少し記事にしてみました。
コードペタペタ貼っただけなので、各自雰囲気で理解してください。
ドラッグ・アンド・ドロップ周りでこれがいいよとか他にあったら教えてください。

参考

react-dnd これだけ抑えておけばおk
React DnDでスマホでもドラッグアンドドロップ

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

2019年版Vue.jsを使ってる人には必ず知っていてほしいVue.jsの武器とドキュメントに書かれていないコンポーネントやメンテナンスの際に役立つTips

はじめに

私はVue.js with Vuexを使った業務で1画面30APIを叩く必要のある画面から、たったの数APIしか叩かないけれど、代わりにUIがとても機能的で複雑な画面まで設計し、構築しました。
もちろん、Vue.jsのコンポーネントシステムをフル活用し、Vuexを入れていないプロジェクトの経験もあります。
現在は構築したシステムを保守・運用しています。
また、勉強の為にReact、 Angular、 最近はElm等にも少し触れています。
その際に得られたノウハウ、Vue.jsが他のライバルフレームワークと比べた際に現時点で本当に勝っている利点やノウハウを言語化し、共有出来たらと思います。

※ 記事の内容に意見がありましたら直接編集リクエストをください。

Vue.js含めどのフレームワークも利点・欠点があります

せっかくなので自分が把握しているVue.jsの利点と欠点を説明しようと思います。

利点

Vue.jsの武器は単一ファイルコンポーネントとそのシンタックスです!

コンポーネントシステムやファイルの粒度は思想は違えどReactでも似たような事はできますし、Angularでも似たような事はできますが、Vue.jsの場合は1つのコンポーネントを作るのに必要なファイルがたったの1つであり、そのファイルの中に慣れ親しんだhtmlで描画部分を書き、慣れ親しんだ普通のjavascriptでコンポーネントに必要な処理や表示ロジックを書け、css in js, css module, sass, stylus等、好きな方法でそのコンポーネントにだけ当たるスタイルを書けます。
これが他のフレームワークと比べて圧倒的に見やすく、学習していない状態でもなんとなく触れるレベルでわかりやすく、シンプルでしょう。

利点を妨げる要素

Vuexはできる限り入れないほうが良いよという話

Vuexを入れるメリット

  • Vuexを入れるとFluxライクなアーキテクチャが採用でき、model層でのデータ管理が可能になります。
  • 設計の仕方によってはmodel同士の結合度等を自由に操作でき、かなり疎結合にもできます。 また、これはテストしやすいです。
  • etc...

デメリット

  • model同士の結合度をコントロールするのはかなりの設計面の技術力が要求され、苦しんでいる話をよく耳にします。
  • そこそこjavascript力も要求されます。
  • Vue本来の1ファイルで完結するような圧倒的な見通しの良さがなくなってしまします。
  • model層が発生するとそれだけ複雑になりますし、イベントが誰から発行されたのかの管理も難しいです。
  • Typescriptを入れない場合は、Vuexとのミスバインドもよく発生しますし、Vuexのロードマップにはこれから破壊的な変更が沢山入る事が記されています。
  • Typescript対応も凄く困難が多いです。
  • Vueは実はReactのようなコントロールドコンポーネントが作れません。dataがこれだから、表示されている値はこれという保証が一切ないです。
  • Stateが代入構文しか受け付けない為、Object.assignですり抜ける以外にStateでUnionTypeを使う方法がないです

Vue + Vuexの感想

もちろん部分的に状態管理が本当に必要な箇所があれば入れるのは良いと思いますが、本当に最低限の利用に留めた方が良いと思います。
しかし現時点で全体的にVuexを入れるとなると、Vue.jsの長所であるSFCの恩恵が大分減るので、他のフレームワーク使ったほうが良いと思いました。

欠点

顕在化した業界主流とのズレ

現時点では、ある程度の中・大規模開発ではTypescriptを利用するのが業界の主流です。
Vue.jsの場合だとここで1歩遅れを取っていて、その理由を以下に上げます。

  1. vueファイルはオリジナルファイル形式であるため、エディターでvueのサポートをするのが大変なのです。2019年3月1日時点だと、vscodeではvueファイルのtsのバージョンと本当に入れているtypescriptのバージョンに差分がでています。もしかしたら自分が使っているのよりも良いvueの構文解析ツールがあるかもしれませんが、このvueファイルのメンテナンスや出来に影響を受ける開発者さんは多いでしょう。また、構文解析ツールを作る人達の大変な声をよく耳にします。

  2. ファイルの拡張子がvueであり、オリジナル構文なため、何がそのファイルからexportされているか既存の解析ツールの資産だとわかりません。例えばimport Hoge from Hoge.vueとした場合、現時点だと、とりあえずvueインスタンス又はvueのコンポーネントオプションがdefaultでexportされているよ!ぐらいに型をつけるのが精一杯でしょう。

改善

Vue.jsの今後

Vue.jsのロードマップはここにあります。
https://github.com/vuejs/roadmap

Evan Youさんが言うには

  • Vue.jsをTypescriptで作り直し、vueのtemplate内でも型は効くようになるし、tsxのサポートも強化するようです。
  • どうしようもない部分にanyとかの型を付けていたのもきっと消えると期待しています。
  • import Hoge from Hoge.vueとした時もちゃんと型がついているのを期待していいのではないかと思います。
  • Vuexでよく型が辛いと言われますが、その部分も改善すると思われます。
  • VueのClass Base Componentをネイティブにサポートするなどの細かい追加もあります。

なので、Vue.js + Typescriptをしっかりとやりたい場合はVue.js3のリリースを待つのが良いのではないかとおもいます。

フロントを新しく作る場合どのフレームワークで作ればいいの?

個人的には

  • javascriptがそこまで得意でもないフロント初学者 => Vue.js
  • Typescript + classベースの構文が好き => Angular
  • 関数型の構文が好きな場合はReact
  • バックエンドをScala、Haskellとかで書いてたことがあり、且つ好き => Elm

とかでいいんじゃないのだろうかと思います。

まとめ

確かに受けれる恩恵もありますが、もしVuexを全面的に入れるぐらいなら現時点だと他のフレームワークで書いたほうが良いのは間違いないでしょう。

Vue.jsの最も協力な武器であるSFCの質を上げるTips

ほとんどのバグは変数から来ます。
もし全ての値が定数なら状態から来るバグはほとんど無くなるでしょう。
ここではこの変数や式を極力減らしたり、見通しを更によくするTipsを紹介したいと思います。

htmlやvue標準で用意されているタグやコンポーネント名と同じ名前のコンポーネント名を付けない

もしhtmlやvue標準で用意されているタグやコンポーネント名と同じ名前のコンポーネント名を付けたい場合は、The prefixを付けるなどして、必ず別名にしてください。
参考リンク

自分が1度遭遇したバグなんですが、プロジェクト全体にprettierでフォーマットかけて、かならずprettier通してないとCIが通らないようにした際にヘッダー部分とか色んなコンポーネントが表示されなくなり、アプリが一時的に壊れました。
それの原因は、Header部分のコンポーネントをHeaderと書いており、prettierによってheaderに修正されたからです。
その影響により、htmlのheaderタグがただ表示されるようになってしまいました。
他にも同じようなコンポーネントにいくつか出会いましたが、この命名方式はそのようなバグから逃れる為です。

templateで必要じゃないmethodは極力vueに定義しない

これはどのmethodがそのcomponentで使われているのかを簡単に把握する為です。

悪い例:

export default {
  data() {
    return {
      users: [],
      loading: true
    };
  },
  mounted() {
    this.getUsers();
  },
  methods: {
    async getUsers() {
      const { data } = await this.$axios.get('/api/users');

      this.users = data.users;
      this.loading = false;
    }
  }
};

例えばこちら、getUsersがなんかtemplate内でも使われてそうな雰囲気があります。
また、もしかしたら他のcomponentthis.$refs.hoge.getUsers()ってコードを書いて、そのメソッドを実行している可能性もあります。
チーム開発で、それはしていない、このメソッドは使ってない!消せる!って言い切るには、全文検索するしか今の所方法がないです。このTipsを適用すればそのような不安もある程度緩和できると思います。

改善例:

async function getUsers(axios) {
  const { data } = await axios.get('/api/users');
  return data.users
}

export default {
  data() {
    return {
      users: [],
      loading: true
    };
  },
  async mounted() {
    this.users = await getUsers(this.$axios);
    this.loading = false;
  },
};

改善例では、このコンポーネントはmount時にデータ取得を1度だけするんだなってのが凄く明瞭だと思います。
また、他のcomponentthis.$refs.hoge.getUsers()ってコードを書いて、そのメソッドを実行している可能性が完全になくなりましたね!

dataを極力定義しない

Vue.jsでコンポーネントを定義する際ついdata()に沢山変数を定義しちゃいますよね。
しかし、dataはいわゆるインスタンス変数です。
もしそのdataの値が定数とかpropsから算出できる値だったりした場合できるだけcomputedに移してあげましょう。
なぜならsetterがないcomputed はread onlyだからです。read onlyは変更される心配がないためバグを減らしてくれるとても素晴らしいものです!

悪い例:

export default {
  props: {
    user: {
      type: Object,
      required: true,
    },
  },
  data() {
    const birthdayDate = new Date(this.user.birthday);
    return {
      max: 5,
      birthday: `${birthdayDate.getFullYear()}/${birthdayDate.getMonth() + 1}`
    };
  },
};

悪い例ではmaxやbirthdayがtemplate内でも変更される心配があります。
また、user propの値が変わってもbirthdayは再計算されることはないでしょう。

改善例:

export default {
  computed: {
    max: () => 5,
    birthday() {
      const birthdayDate = new Date(this.user.birthday);
      return `${birthdayDate.getFullYear()}/${birthdayDate.getMonth() + 1}`;
    },
  },
};

改善例ではmaxやbirthdayがtemplate内でも変更される心配がない事がすぐにわかります。
また、user propの値が変わってもcomputedのbirthdayは正しい値をリアクティブに再計算してくれます。
改善例の方がこれはどういう値か伝わってきますね!
しかもダメな例と比べ状態(変数)を1つ減らせました!つまりバグの原因が1つ減りました!おめでとうございます!

template内でできるだけ式を書かない

template内でdataなどを直接変更したりイベントを発火したりするコードを直接書くことはよくありません。
それには大まかに2つの理由があります。

  • 変数を定義している場所と変更されている場所が遠すぎて視認性にすぐれない
  • methodで定義すれば式にメソッド名という形式でこのカプセル化された式はどういう意図をもった式なのかを簡単に伝えられる
    • template内に直接式を書いた場合は逆にこの式はどういう式かをコードを見た人全員が推測しなければなりません。つまりハイコンテキスト、空気を読めって事です。メンテする際に大抵困ります。

悪い例:

<template>
  <button @click="count++ && $emit('change', count)">ボタン</button>
</template>


<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
};
</script>

改善例:

<template>
  <button @click="incAndNotify()">ボタン</button>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    incAndNotify() {
      this.count++;
      this.$emit('change', count);
    },
  },
};
</script>

templateタグを最大限に有効活用

このようなコードをたまにみます。
お世辞でもスマートとは言い難いですね。

悪い例:

<template>
  <div>
    <button @click="inc()">ボタン</button>

    <div v-if="isSmallCount">これはとても小さな値です!</div>
    <div v-if="isSmallCount">値をもっともっと増やしてください!</div>

    <div v-if="!isSmallCount">これはとても大きな値です!</div>
    <div v-if="!isSmallCount">すごいです!</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  computed: {
    isSmallCount() {
      return this.count < 5;
    },
  },
  methods: {
    inc() {
      this.count++;
    },
  },
};
</script>

改善例:

<template>
  <div>
    <button @click="inc()">ボタン</button>

    <template v-if="isSmallCount">
      <div>これはとても小さな値です!</div>
      <div>値をもっともっと増やしてください!</div>
    </template>

    <template v-else>
      <div>これはとても大きな値です!</div>
      <div>すごいです!</div>
    </template>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  computed: {
    isSmallCount() {
      return this.count < 5;
    },
  },
  methods: {
    inc() {
      this.count++;
    },
  },
};
</script>

改善例では、templateタグ内にtemplateタグが使われています。
どういうことでしょうか?
一度isSmallCountが真の時にDOMがどのようにマウントされるのかを見てみましょう!

<div>
  <button @click="inc()">ボタン</button>
  <div>これはとても小さな値です!</div>
  <div>値をもっともっと増やしてください!</div>
</div>

なんてことでしょう!templateタグがどこにも見当たりません!
そうtemplateタグはなんとマウントされる際に表示されないのです!
これはv-ifなどの式を書く際にとても重宝できます!スコープはありませんが、ブロック的なものを表現できますしね!

※ もちろんv-forなどにも使えますよ?
※ 注意: v-showなどスタイルを当てる奴は反応しません

フラグを極力減らす

通信をするコンポーネントでは通信中状態と通信していない待機状態があります。
例えばこれを2つのフラグで管理していた場合不整合が発生する可能性が出ます。
これはとても不健康な状態です。きっと将来待機状態なのに通信状態という意味不明な状態が発生するでしょう。

悪い例:

<template v-if="isInitializing">
  <div>初期化中です。。。?</div>
</template>

<template v-else-if="isStandby">
  <div>初期化完了です?</div>
</template>

<script>
export default {
  data() {
    return {
      isStandby: false,
      isInitializing: true,
    };
  },
};
</script>

悪い例では先程も言ったようにフラグの整合性を保証できるものが何もありませんね。
ですのでこれらを1つの状態フラグで管理するようにします。
名前は適当にcurrentStateで良いでしょう。

改善例:

<template v-if="isInitializing">
  <div>初期化中です。。。?</div>
</template>

<template v-else-if="isStandby">
  <div>初期化完了です?</div>
</template>

<script>
const IS_STANDBY = 'IS_STANDBY';
const IS_INITIALIZING = 'IS_INITIALIZING';

export default {
  data() {
    return {
      currentState: IS_STANDBY,
    };
  },
  computed: {
    isStandby() {
      return this.currentState === IS_STANDBY;
    },
    isInitializing() {
      return this.currentState === IS_INITIALIZING;
    },
  },
  methods: {
    toStandby() {
      this.currentState = IS_STANDBY;
    },
    toInitializing() {
      this.currentState = IS_INITIALIZING;
    },
  },
};
</script>

やりました!改善例では変数を1つ減らし、不整合が絶対に起こり得なくなりました!
今回の例はとてもシンプルですが、画面のタブを管理する時など状態が多くなればなるほど役に立つテクニックになります。

必須propsに必ずrequired trueを付ける

必須propsには必ずrequired trueを付けるべきです。
たまにあるのが、必須propsなのにdefault値だけを指定してrequired trueを設定しないケースです。

悪い例:

export default {
  props: {
    account: { type: Object, default: null },
    shokai: { type: Object, default: null }
  },
};

なぜこれが悪いのかというと、account propに何か入れて欲しいのに、入れ忘れた場合でも一切警告がでなく、バグ発見が困難になります。
また、本当に任意であるpropがどれか見分ける難易度が上がってしまいます。

改善例:

export default {
  props: {
    // account: { type: Object, default: null },
    // shokai: { type: Object, default: null }
    account: { type: Object, required: true },
    shokai: { type: Object, required: true }
  },
};

やりました!
改善例ではaccountにpropをバインドし忘れてもエラーがでるのですぐに気付けますね!

汚いコンポーネントをスマートにするテクニック

レイアウトやアニメーションに関するコードと表示用コンポーネントのコードが一緒になっているコード等が対象です。
大抵の場合別に良いのですが、v-forでリストを表示して、そのリストの各アイテムにも詳細情報の開閉ボタンがあり、クリックされたら詳細が開示されるなどのコードがあります。
まぁ大抵ここまでのコードを1つのコンポーネントで書くと、とてもではないですがメンテナブルなコードとは言い難いですね。バグが発生しても各アイテムの開閉状態のコードなど色んなコードが一箇所に書かれているため何が原因か特定するのにも時間がかかりますし、破壊する恐れもあります。
では、slotに渡されたアイテムの開閉だけを担当するコンポーネントと、専用のslotをいくつか用意したレイアウト用のコンポーネントを作成し、それをメインとなるコンポーネントで使ってはどうでしょうか。コードがとても見通しがよくなります。
また、メインとなるコンポーネントからはきっと大量のインスタンス変数が消えてバグが減ったかと思われます。

一応これは開閉だけするコンポーネントのサンプルです。
これは自分がフォークして作ったライブラリで、メンテしてないのでお世辞にも綺麗なコードとは言えませんが、参考程度に。
https://github.com/kahirokunn/vue-slide-up-down-component

OSSのUIライブラリを利用しましょう

1からプロダクトを作成する際にdialog、tooltip、form、validationなど他にも目眩がする程の大量のコンポーネントを作る必要があります。しかも自社でそういうのを全部書いてるときっと粒度も何もかもがバラバラでしょう。しかもレスポンシブ対応もできるだけしたい!あぁ、これはとても大変です!
そういう場合は既存のOSSからUIライブラリを拝借しましょう!
その際に重要なチェック項目として

  • 活発
  • ドキュメント、サンプルコードが豊富
  • レスポンシブ

などがあります。
ちなみによくできたUIライブラリはCSSは基本上書きできますので大抵のデザイン要件を満たせます!
それでもデザイン要件を満たすのが辛い場合はデザイナーさんと相談しましょう!このUIライブラリを使っていてできるだけこれに沿って工数を減らしたいといえばきっと協力してくれるでしょう!
ちなみに私のオススメはVuetifyです。これはとてもよくできていて、活発で、ドキュメントやサンプルコードが豊富でレスポンシブ対応です!

※ 自社専用のUIライブラリ別リポジトリで用意し、上手くメンテできるほどの潤沢なリソースがある環境は例外とします。

OSSのUIコンポーネントなどを積極的に利用しましょう

私はよく全然活発でなくてメンテされていなくても現在のニーズにあったOSSのUIコンポーネントがあった場合はとりあえず利用します。
それはとても怖いことではないでしょうか?もしそのOSSがバグっていたらと思うとゾッとするのではないですか?そう思われる方はきっと沢山いるでしょう。
しかし自分が書いてもそのコードはバグってないと保証できるのでしょうか?
かのマーク・ザッカーバーグもこう言っています。

これみて.jpg

そう、とりあえず使ってみればいいでしょう!
よく運用している途中でそのOSSのバグを見つけたりします。
もし見つけた場合は、正しく動くように利用していたOSSのコードを参考に自分で改善した奴をnpmに公開したりします。もしくは利用していたOSSへ感謝の気持ちを込めてプルリクエストを作ってあげましょう!
そう、結局は自分で書いても他の人が書いてもバグる時はバグるのでとりあえず使ってみてバグったら直しましょう!

最後に

如何だったでしょうか?
最近あまり記事を書いていないので久しぶりに前の記事の延長として書いてみました。

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

use-react-redux-context で redux 環境下で React Hooks を使う

https://github.com/mizchi/use-react-redux-context

というライブラリを作りました。

yarn add use-react-redux-context

で使えます。が、 peer deps として react react-dom react-redux redux がいるので、今までの redux スタック全部盛りでもあります。

これは何

ReactRedux connect を、Context API と useContext で置き換えようとしたものです。

その過程でパフォーマンスチューニングする余地を大量に入れ込んでいます。

hooks で redux を置き換えるものではなく、react-redux と協調するものです。

簡単な使い方

ReactRedux の Provider の下で、mapState される props を持つ Context を生成し、それを useContext できる、というもの。

import { Provider as ContextProvider, Scope } from "use-react-redux-context";
import { Provider as ReactReduxProvider } from "react-redux";

const scope = new Scope();
const RootStateContext = scope.createContext(s => s);
const All = () => {
  const all = useContext(RootStateContext);
  return <pre>{JSON.stringify(all)}</pre>;
};

const store = createStore(reducer); // create your reducer
const el = (
  <ReactReduxProvider store={store}>
    <ContextProvider scope={scope}>
      <All />
    </ContextProvider>
  </ReactReduxProvider>
);

直接 React.Context を生成するので、そのまま useContext できます。

仕組み

(ここから先は雑に使うだけなら読まなくても大丈夫です。)

react-redux@6 以降は、親子関係に基づくデータの受け渡し方法が、実装が古い prop-types ベースの  Context API から New Context API になりました。

Context – React

なので、内部的に提供される ReactReduxContext と hooks の useContext を使うと、次のように使うことができます。

import React, {useContext} from "react"
import { ReactReduxContext, Provider } from "react-redux";
functin App(){
  const {store, storeState} = useContext();
  return <pre>{JSON.stringify(storeState)}</pre>
}

ReactDOM.render(<Provider store={...}><App /></Provider>, el);

ただ、 ReactReduxContext をこのまま使うと、 useContext したすべてのコンポーネントは、redux のすべての変更に対して render が走ってしまうので、すぐにパフォーマンス問題にぶち当たってしまいます。

既存の connect の最適化手法

react-redux の connect は、mapState された値の shallow equal 比較によって再更新を抑制していました。

const MyConnectedComponent = connect(s => ({value: s.counter.value}))(MyComponent);

このとき MyComponent が更新されるのは prevState.value !== nextState.value のときだけです。

最適化: 事前に mapState される Context を、その数だけ作ればいいのでは

use-react-redux-context の思想は、 Context が事前に宣言されたスコープになるなら、それを全部事前に宣言してしまえばいいのでは、というものです。

provider の scope という概念を導入しました。

const scope = new Scope();
const RootStateContext = scope.createContext(s => s);

const All = () => {
  const all = useContext(RootStateContext);
  return <pre>{JSON.stringify(all)}</pre>;
};

この scope に紐づく Context は、redux store が更新されるたびに、一度だけ実行されて状態が更新されます。

これによって、connect されるだけ計算される mapState の実行回数が、宣言される Context の種類の数になりました。

この制約によって、 Component 側の要求によって、 connect を動的に宣言する、という感じの世界観はなくなります。できるにはできますが、シングルトンに依存する次のようなコードになると思います。

import {myScope} from './contexts/my-scope'
const AContext = myScope.createContext(s => s.a);
export function A() {
  const a = useContext(AContext);
  return ...
}

要は connect のコールバックの第二引数、 (state, ownProps) => ... の ownProps が受け取れません。ただ、ownProps を使ってる人をあまり見たことがないので、切り捨てることにしました。

そもそも scope 概念を導入したのは、異なるデータ配分の単位を分割して作りたい場合、その mapState の計算回数の N を分割する余地を残したかったからです。宣言しただけで、使わなくても必ず更新されます。

const a = new Scope();
const X = a.createContext(fn_x);
const Y = a.createContext(fn_y);

const b = new Scope();
const Z = b.createContext(fn_z);

const el = <>
  <Provider scope={a}><WorldA /></Provider>
  <Provider scope={b}><WorldB /></Provider>
</>

useCallback の mapState への応用

hooks の mapState 関数の実行は、 React Context の hooks スコープにいるということで、次のような最適化が可能になりました。

const FooContext = scope.createContext((state, dispatch) => {
  const inc = useCallback(() => dispatch(increment()), []);
  return { ...state.foo, inc };
});

これは hooks の機能を使って、memoizedKeys が同じ時は同じ関数参照を返します。

複雑さで評判の connect の第二引数 bindActionCreator を切り捨てて、(state, dispatch) => mappedState(WithAction) ということにしてしまったことで、TypeScript の推論にも優しく、使うのも簡単になりました。

で、 useCallback とこの dispatch を何度も書くのが大変なので、useBindAction, useBindActions というヘルパを作りました。

const A = scope.createContext(state => {
  const inc = useBindAction(increment);
  // with memoized keys
  // const inc = useBindAction(() => ({type: 'increment', payload: state.value}), [state.value]);
  return { ...state.a, inc };
});

const B = scope.createContext(_state => {
  const actions = useBindActions({ increment, decrement });
  // alternative: with memoized keys map
  // const actions = useBindActions({ increment, decrement }, { increment: [state.value] });

  // recommend spreading for shallow equal
  return { ...actions };
});

これで使い心地がだいぶ良くなったと思います。

最後に

Context + useReducer でだいぶ redux がいらなくなったとはいえ、redux middleware が必要だったり、異なる state が協調しづらいといった問題で、redux の需要は発展形として残り続けると思います。SSRする際も一枚のjsonにまとめないといけない以上、reduxはどうしても必要になります。

その解決策として use-react-redux-context を作ってみました。気に入ったら star お願いします。

https://github.com/mizchi/use-react-redux-context

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

Electron + React + Redux + TypeScript でデスクトップアプリケーションを作るメモ

Electron + React + Redux + TypeScript でデスクトップアプリケーションを作ろう!というモチベーションで、npm iだけですぐ中身を作り始められるテンプレートがあったら便利だなと思ったので個人的メモ。
2019/03/01現在動作確認済みですが、今後バージョンアップで動かなくなったら内容更新したいと思います。

利用する技術

  • Node.js ... サーバーサイドJavaScript
  • npm ... Node.jsのパッケージを管理するツール(= Node Package Manager)
  • Electron ... デスクトップアプリをWeb技術で開発できるフレームワーク
  • React ... UI構築ライブラリ
  • Redux ... state(状態)管理フレームワーク
  • TypeScript ... JavaScriptと互換性があり、静的型付けやクラス設計が可能なプログラミング言語
  • webpack ... JavaScriptモジュールバンドラ

前提条件

Node.jsがPCにインストールされていること

構成

package.json ... npmライブラリの依存関係を記述
tsconfig.json ... TypeScriptのコンパイルオプションを定義
tslint.json ... コンパイラが行う構文チェックのルールを設定
webpack.config.js ... webpackのビルド設定
main.js ... Electronのメインプロセス
index.html ... UIとなるHTML
ts/index.tsx ... React Component
ts/components/FoundationStyles.ts ... 共通スタイル設定
.gitignore ... Gitリポジトリで無視するファイルを設定

各ファイルの初期内容

package.json

package.json
{
  "name": "electron-react-sample-app",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "electron ./"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "clone": "^2.1.2",
    "react": "^16.8.3",
    "react-dom": "^16.8.3",
    "react-redux": "^6.0.1",
    "redux": "^4.0.1",
    "reset-css": "^4.0.1",
    "styled-components": "^4.1.3",
    "uuid": "^3.3.2"
  },
  "devDependencies": {
    "@types/clone": "^0.1.30",
    "@types/react": "^16.8.6",
    "@types/react-dom": "^16.8.2",
    "@types/react-redux": "^7.0.1",
    "@types/redux": "^3.6.0",
    "@types/styled-components": "^4.1.11",
    "@types/uuid": "^3.4.4",
    "css-loader": "^2.1.0",
    "electron": "^4.0.6",
    "style-loader": "^0.23.1",
    "ts-loader": "^5.3.3",
    "tslint": "^5.13.0",
    "tslint-loader": "^3.5.4",
    "typescript": "^3.3.3333",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.2.3"
  }
}

tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "jsx": "react",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "sourceRoot": "./tsx",
    "inlineSourceMap": true,
    "inlineSources": true
  },
  "include": [
    "./ts/**/*"
  ]
}

tslint.json

tslint.json
{
    "defaultSeverity": "error",
    "extends": [
        "tslint:recommended"
    ],
    "jsRules": {},
    "rules": {
        // ここにルールを追加
    },
    "rulesDirectory": []
}

webpack.config.js

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

module.exports = {
    target: 'node',
    entry: './ts/index.tsx',
    cache: true,
    mode: 'development',
    devtool: 'source-map',
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'index.js'
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader'
            },
            {
                test: /\.tsx?$/,
                enforce: 'pre',
                loader: 'tslint-loader',
                options: {
                    configFile: './tslint.json',
                    typeCheck: true,
                },
            },
            {
                test: /\.css$/,
                loaders: ['style-loader', 'css-loader'],
            }
        ],
    },
    resolve: {
        extensions: [
            '.ts',
            '.tsx',
            '.js',
        ]
    },
};

main.js

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

let win;

function createWindow() {
    win = new BrowserWindow({
        width: 800,
        height: 600
    })
    win.loadFile('index.html')
    if (process.argv.find((arg) => arg === '--debug')) {
        win.webContents.openDevTools()
    }
    win.on('closed', () => {
        win = null
    })
}

app.on('ready', createWindow)

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit()
    }
});

app.on('activate', () => {
    if (win === null) {
        createWindow()
    }
});

index.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>Sample App</title>
</head>
<body>
    <div id="contents"></div>
    <script src="dist/index.js"></script>
</body>
</html>

ts/index.tsx

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

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

ReactDom.render(
    <p>Hello, World!</p>,
    container,
);

ts/components/FoundationStyles.ts

FoundationStyles.ts
import 'reset-css/reset.css';

import { createGlobalStyle } from 'styled-components';

// グローバル スタイル 定義
// tslint:disable-next-line:no-unused-expression
export const GlobalStyle = createGlobalStyle`
    html, body {
        font-family: "Meiryo UI";
        font-size: 12pt;
        height: 100%;
        width: 100%;
    }
`;

.gitignore

# See https://help.github.com/ignore-files/ for more about ignoring files.

# dependencies
/node_modules

# testing
/coverage

# production
/build
/dist

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
.idea

node modules(必須)

npm install --save

  • react
  • react-dom
  • redux
  • react-redux

npm install --save-dev (型定義ファイル含む)

  • @types/react
  • @types/react-dom
  • @types/redux
  • @types/react-redux
  • electron
  • typescript
  • tslint
  • webpack
  • webpack-cli
  • ts-loader
  • tslint-loader
  • style-loader
  • css-loader

node modules(任意)

npm install --save

  • styled-components ... CSS in JavaScriptのためのライブラリ
  • reset-css ... ブラウザのデフォルトスタイルのリセットと共通スタイルの定義
  • uuid ... 一意のIDを生成
  • clone ... オブジェクトの複製を生成

npm install --save-dev (型定義ファイル含む)

  • @types/styled-components
  • @types/uuid
  • @types/clone

今回のGitリポジトリ

ダウンロードして
$ npm i
$ npm run build
$ npm start
でElectronでHello, world!が立ち上がるはず。
これで環境構築2~3分ですぐ中身作り始められる〜スッキリ!

https://github.com/aimy-07/electlon-react-sample-app

参考サイト

めっちゃわかりやすかった!!素晴らしや!
https://qiita.com/EBIHARA_kenji/items/25e59f7132b96cb886f3

おわりに

今回は以上です。
当方Web開発初学者のため、間違い等ありましたらご指摘お願いいたします。
ありがとうございました。

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

React開発ノウハウメモ(随時更新)

少し前までAngularを使って開発してきましたが、年明けごろからReactを使い始めたので、自分なりのノウハウをまとめておきます。随時更新予定です。

使っている言語・ライブラリはReact, TypeScript, RxJSです。
RxJSはAngularで使っており慣れているのでReactでも引き続き使っていこうと試行錯誤しています。型定義の無いライブラリが多く面倒なので、なるべく生のReactで解決しようとしていますが、今後は状態管理に別のライブラリを使うかもしれません。


型付け

  • statepropsのメンバーはComponent内では書き換えてはならないので、すべてのメンバーにreadonlyを付ける。全メンバーにreadonlyを付けるのは面倒なので以下のように定義している。
interface IProps extends Readonly<{
  member1: number,
  member2: string,
  member3: number[],
}>{}

これで

interface IProps {
  readonly member1: number;
  readonly member2: string;
  readonly member3: number[];
}

と同じ意味になる(はず)。

  • スタイリングはエラー発見しやすいCSSinJSを使っている。CSSオブジェクトの型定義を以下のように作成した。
import { CSSProperties } from 'react';

export interface CSSobj { readonly [key: string]: CSSProperties; }

使い方

const css: ICSSobj = {
  tableWrapper: {
    overflowX: 'auto',
  },
  spacer: {
    flexGrow: 1,
  },
  footer: {
    display: "flex",
    flexDirection: "row",
    alignItems: "center",
  },
  resetButtonWrapper: {
    padding: "7px",
  },
};

関数コンポーネント(FC)

ステートレスコンポーネントを関数で簡単に書けるのがReactを使い始めて一番良いと思ったところ。

自分は基本的に、state管理を行う親コンポーネント(class)と、ロジックをほぼ持たずCSSスタイリング等を行うview専用の子コンポーネント(FC)の親子組を作るようにした。

class InputWithReset extends React.Component<Readonly<{
  placeholder: string;
  valueChange: (v: string) => void;
}>, Readonly<{
  value: string
}>> {

  state = {
    value: '',
  };

  valueChange = (value: string) => {
    this.setState({ value: value });
    this.props.valueChange(value);
  }

  resetClick = () => {
    this.setState({ value: '' });
    this.props.valueChange('');
  }

  render = () => (
    <InputWithResetView
      value={this.state.value}
      placeholder={this.props.placeholder}
      valueChange={this.valueChange}
      resetClick={this.resetClick}
    />
  )
}
const css: ICSSobj = {
  input: {
    minWidth: "120px",
  },
};

const InputWithResetView = (props: Readonly<{
  placeholder: string;
  value: string;
  valueChange: (value: string) => void;
  resetClick: () => void;
}>) => {
  const onInput = (ev: React.ChangeEvent<HTMLInputElement>) =>
    props.valueChange( ev.target.value || '' );

  return (
    <FormControl>
      <InputLabel shrink htmlFor="input">{props.placeholder}</InputLabel>
      <Input
        style={css.input}
        id="input"
        type='text'
        value={props.value}
        onChange={onInput}
        endAdornment={
          <InputAdornment position="end">
            <IconButton
              aria-label="Reset Input"
              onClick={props.resetClick}
            >
              <ClearIcon />
            </IconButton>
          </InputAdornment>
        }
      />
    </FormControl>
  );
};

event.target.valueを取り出すようなhtml要素に近い部分の整形はviewの方で行うようにしている。
import文も整理されるので親子は別ファイルに書くことにした。

Props変更時の処理

class componentにおいてpropsの値が変わったとき、renderは呼ばれるがconstructor等に記述したpropsに依存する変数は再計算されない。

propsが変わったときにrender以外の処理を行いたい場合については、公式ページの getDerivedStateFromProps の説明に書かれている。stateをpropsに依存するようにするのはバグの元なのでgetDerivedStateFromPropsメソッド以外の方法を推奨しているようだ。
対応方法が箇条書きで3つ書かれているが、propsの値を使った変数の値を再計算したい場合、2つ目と3つ目が該当するように思われる。

どれが適しているかはケースによるが、自分はconstructor内のロジックも含めて全体的に再計算したかったため3つ目を採用した。親コンポーネントにおいてこのコンポーネントをkey={Date.now()}を付けて呼ぶことで、keyの変更によりコンポーネントが再生成されることを利用するという方法だ。

interface IProps extends Readonly<{ data: any }>{}

const ParentFC = (props: IProps) => (
  <div>
    {!!props.data &&
      <Child
        key={Date.now()}  // recreate this component every time when props change
        data={props.data}
      />
    }
  </div>
);


const eq = (prev: IProps, curr: IProps) => (
  (prev.data === curr.data)
);

export const Parent = React.memo(ParentFC, eq);
class Child extends React.Component<IProps, IState> {
  constructor(props: IProps) {
    super(props);
    // propsの値を使った様々な計算
  }

...
}

また、コンポーネント全体を再生成するため高コストであることも考慮し、メモ化も行っている。

2つ目の use a memoization helper をやりたくなかった理由は、本来表示に関するメソッドであるはずのrender内にロジックを書くのが気持ち悪かったので。
props変更時にrenderは呼ばれるので、内部変数の再計算をここで行い、一部のみ変更したい場合に対応するためにメモ化するという方法のようで、必要な再計算が一部のみであることが分かっていればこちらを採用した方がパフォーマンスは向上するのかもしれない。

RxJS

複雑な状態管理にはRxJSを使っている。Angularのようにasyncパイプなどはなさそうなので、subscribeをちゃんと書く必要があるが、どこに書くのか分からなかったのでメモ。

Angularでやっていた時と同じく、takeWhilealive変数を使って自動unsubscribeさせるパターンを使っている。

export class A extends React.Component<IProps, IState> {
  state = {
    data: [],
  };

  private alive = true;

  componentWillUnmount() { this.alive = false; }

  componentDidMount() {
    this.data$.takeWhile( () => this.alive )
    .subscribe( v => {
      this.setState({ data: v });
    });
  }

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

Reactによる開発ノウハウメモ(随時更新)

少し前までAngularを使って開発してきましたが、年明けごろからReactを使い始めたので、自分なりのノウハウをまとめていきたいと思います。

使っている言語・ライブラリはReact, TypeScript, RxJSです。RxJSはAngularで使っており慣れているのでReactでも引き続き使っていこうと試行錯誤しています。

型付け

  • statepropsのメンバーはComponent内では書き換えてはならないので、すべてのメンバーにreadonlyを付ける。全メンバーにreadonlyを付けるのは面倒なので以下のように定義している。
interface IProps extends Readonly<{
  member1: number,
  member2: string,
  member3: number[],
}>{}

これで

interface IProps {
  readonly member1: number;
  readonly member2: string;
  readonly member3: number[];
}

と同じ意味になる(はず)。

  • スタイリングはエラー発見しやすいCSSinJSを使っている。CSSオブジェクトの型定義を以下のように作成した。
import { CSSProperties } from 'react';

export interface CSSobj { readonly [key: string]: CSSProperties; }

使い方

const css: ICSSobj = {
  tableWrapper: {
    overflowX: 'auto',
  },
  spacer: {
    flexGrow: 1,
  },
  footer: {
    display: "flex",
    flexDirection: "row",
    alignItems: "center",
  },
  resetButtonWrapper: {
    padding: "7px",
  },
};

関数コンポーネント(FC)

ステートレスコンポーネントを関数で簡単に書けるのがReactを使い始めて一番良いと思ったところ。

自分は基本的に、state管理を行う親コンポーネント(class)と、ロジックをほぼ持たずCSSスタイリング等を行うview専用の子コンポーネント(FC)の親子組を作るようにした。

class InputWithReset extends React.Component<Readonly<{
  placeholder: string;
  valueChange: (v: string) => void;
}>, Readonly<{
  value: string
}>> {

  state = {
    value: '',
  };

  valueChange = (value: string) => {
    this.setState({ value: value });
    this.props.valueChange(value);
  }

  resetClick = () => {
    this.setState({ value: '' });
    this.props.valueChange('');
  }

  render = () => (
    <InputWithResetView
      value={this.state.value}
      placeholder={this.props.placeholder}
      valueChange={this.valueChange}
      resetClick={this.resetClick}
    />
  )
}
const css: ICSSobj = {
  input: {
    minWidth: "120px",
  },
};

const InputWithResetView = (props: Readonly<{
  placeholder: string;
  value: string;
  valueChange: (value: string) => void;
  resetClick: () => void;
}>) => {
  const onInput = (ev: React.ChangeEvent<HTMLInputElement>) =>
    props.valueChange( ev.target.value || '' );

  return (
    <FormControl>
      <InputLabel shrink htmlFor="input">{props.placeholder}</InputLabel>
      <Input
        style={css.input}
        id="input"
        type='text'
        value={props.value}
        onChange={onInput}
        endAdornment={
          <InputAdornment position="end">
            <IconButton
              aria-label="Reset Input"
              onClick={props.resetClick}
            >
              <ClearIcon />
            </IconButton>
          </InputAdornment>
        }
      />
    </FormControl>
  );
};

event.target.valueを取り出すようなhtml要素に近い部分の整形はviewの方で行うようにしている。
import文も整理されるので親子は別ファイルに書くことにした。

Props変更時の処理

class componentにおいてpropsの値が変わったとき、renderは呼ばれるがconstructor等に記述したpropsに依存する変数は再計算されない。

propsが変わったときにrender以外の処理を行いたい場合については、公式ページの getDerivedStateFromProps の説明に書かれている。stateをpropsに依存するようにするのはバグの元なのでgetDerivedStateFromPropsメソッド以外の方法を推奨しているようだ。
対応方法が箇条書きで3つ書かれているが、propsの値を使った変数の値を再計算したい場合、2つ目と3つ目が該当するように思われる。

どれが適しているかはケースによるが、自分はconstructor内のロジックも含めて全体的に再計算したかったため3つ目を採用した。親コンポーネントにおいてこのコンポーネントをkey={Date.now()}を付けて呼ぶことで、keyの変更によりコンポーネントが再生成されることを利用するという方法だ。

interface IProps extends Readonly<{ data: any }>{}

const ParentFC = (props: IProps) => (
  <div>
    {!!props.data &&
      <Child
        key={Date.now()}  // recreate this component every time when props change
        data={props.data}
      />
    }
  </div>
);


const eq = (prev: IProps, curr: IProps) => (
  (prev.data === curr.data)
);

export const Parent = React.memo(ParentFC, eq);
class Child extends React.Component<IProps, IState> {
  constructor(props: IProps) {
    super(props);
    // propsの値を使った様々な計算
  }

...
}

また、コンポーネント全体を再生成するため高コストであることも考慮し、メモ化も行っている。

2つ目の use a memoization helper をやりたくなかった理由は、本来表示に関するメソッドであるはずのrender内にロジックを書くのが気持ち悪かったので。
props変更時にrenderは呼ばれるので、内部変数の再計算をここで行い、一部のみ変更したい場合に対応するためにメモ化するという方法のようで、必要な再計算が一部のみであることが分かっていればこちらを採用した方がパフォーマンスは向上するのかもしれない。


随時更新予定だが、とりあえず今日はここまで。

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

手っ取り早くReactのJSXを試してみる

Reactが今どきだって聞いたけど、JSXという特殊な記法を使わないと真価が発揮されないと聞いた。
でもJSXってコンパイルが必要なんだよね。
Node.jsは知ってるけど、WebpackとかGruntとか使ったことない。
JSXを手軽に試してみたいんだけどなあ。。。

そんなWeb開発弱者の僕らのためのメモ。

手順

  1. CDNを使ってコードを書く
  2. Node.jsをインストールする
  3. Browsersyncをインストールする
  4. Brousersyncを実行する

1. CDNを使ってコードを書く

CDN (Content Delivery Network)で提供されているライブラリを使わせてもらいましょう。

最低限のHTMLを用意します。

index.html
<body>
    <div id="hello-example"></div>

    <script src="https://unpkg.com/react@15/dist/react.min.js"></script>
    <script src="https://unpkg.com/react-dom@15/dist/react-dom.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.38/browser.min.js"></script>
    <script type="text/babel" src="app.jsx"></script>
</body>

React公式のJSXをコピペします。

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

ReactDOM.render(
  <HelloMessage name="Taylor" />,
  document.getElementById('hello-example')
);

2. Node.jsをインストールする

Windowsな僕らはNode.js公式からインストーラーを取得して実行するだけです。
Node.jsとnpm(パッケージ管理ツール)がインストールされます。

3. Browsersyncをインストールする

npmでのパッケージインストールは、ディレクトリローカルにインストールする方法とグローバル環境にインストールする方法があります。
あとで消すこともできますし、とりあえずグローバルに入れてみましょう。-gをつければグローバル環境です。

npm install browser-sync -g

4. Browsersyncを実行する

browser-sync start --server --file "*" --watch

実行すると、Webサーバーが起動してlocalhost:3000がブラウザで開かれます。
ファイルを更新すると、更新がすぐにブラウザに反映されます。

参考

React+JSXのコンパイル(Babel, browserify, webpack)
Browsersyncを利用してお手軽ブラウザ確認環境をつくろう - メドピア開発者ブログ

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