水無瀬のプログラミング日記

プログラムの勉強した時のメモ日記

React+Redux+TypeScript事始め

はじめに

普段はAngularを使っているので、他のフレームワークを触ってみようと思った今日このごろ。
Vueは超絶軽く触ったことがあるので今回はReact。
JSXがパッと見とっつきにくかったので後回しにしてたけど、ちゃんと向き合うことにする。

TL;DR.

今回作ったサンプルコード

インストール

手作業でファイルを作ってもいいけど、Facebook作成のcreate-react-appがあるのでそれを使う。

$ npm install -g create-react-app
# TSを使いたいからオプション指定
$ create-react-app react-sample --typescript

サンプルプログラムを作る

いつも?通りToDoリストを作ることにする。 今回Reactのサンプルなので、せっかくだからReduxも導入することにする。
インストールは下記の通りでOK。

# 必要なライブラリのインストール
$ npm install -save redux react-redux

# 実装時楽したいので型定義もインストール
$ npm install --save-dev @types/react @types/react-redux

サンプルの実装はReduxの公式サイトのサンプルプログラムを参考にしてTSへリファクタしていくことにする。 今回のリファクタリングの方針は下記の通りにした。
ちなみにこれがベストかどうかはわからないので、あくまで今回の方針とする。

  • DOM作ってるところは.tsxにする
  • component周りはclassにする
  • propTypesの代わりにTSのinterfaceを使う

サンプルコード

全量は流石に多いのでGitに上げた。
ActionとかReducerは型つけたぐらいなので、Componentとinterfasceをサンプルとして載せとく。

Component

import React, { Component } from 'react';
import { ITodoProps } from '../commons/interfaces';

export default class Todo extends Component<ITodoProps, {}> {  

  render() {
    return (
      <li
      onClick={this.props.onClick}
      style={{
        textDecoration: this.props.completed ? 'line-through' : 'none'
      }}
    >
      {this.props.text}
    </li>
    );
  }
}

interface

export interface ITodoProps {
    completed: boolean;
    id: number;
    onClick: any;
    text: string
}

export interface ITodoListProps {
    todos?: Array<ITodoProps>;
    toggleTodo: any;
}

export interface ILinkProps {
    active: boolean;
    children: string;
    onClick: any;
    filter: string
}

export interface IState {
    todos: Array<any>;
    visibilityFilter: string;
}

まとめ

今回は簡単にReact + Reduxのチュートリアルをやってみた。
JSX(TSX)の書き方はとっつき肉かなって思ってたけど、意外とそうでもなかった。
Reactが学習コスト低めって言われている理由がちょっとわかった気がする。
そのうちオリジナルの何かを作ろうと思った。
それでは今回はこの辺で。

参考サイト

IntelliJ+Gradle+Springbootセットアップ

はじめに

久しぶりにJavaを触りたくなったので、セットアップするまでの備忘録。
せっかく?なのでIntelliJ + Gradle + Spring bootで作ってみる。

前準備

IntelliJ

公式サイト からダウンロード可。 有料版のUltimateがあるけど、今回はCommunityの方をインストール。

プロジェクト作成

Spring Initializrを使ってテンプレを作成する。
特別変えたところは、Gradle ProjectとDependenciesにはWebを選択肢といた。
ちなみにSpringのバージョンは2.2.1にした。

f:id:minase_mira:20190206234644p:plain

プログラム作成

プロジェクトインポート

上記で作ったプロジェクトをインポートする。
IntelliJを起動し、Import Projectで読み込む。

build.gradle

多分デフォルトだとJava8が対象だと思うので、Java11に対応するように少しbuild.gradleを書き換える。

buildscript {
    ext {
        springBootVersion = '2.1.2.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'idea'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
[compileJava, compileTestJava]*.options*.encoding = "UTF-8"

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Java変更

デフォルトでも動くは動くが何も返ってこないはずなので、せめてメッセージが帰ってくるようにする。
ExampleApplication.java(名前は多少違うかも)と同じフォルダにHelloControler.javaを作成する。

package com.example.example;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
public class HelloController {

    @RequestMapping("/")
    public String hello() {
        return "Hello World!";
    }

}

ビルド & 実行

IntelliJの場合、左下のメニューからGradleを選択。
右側にGradleのメニューが出てくるので、リフレッシュ後「Tasks > application > bootRun」を実行。
エラーにならず起動できればOK。

ちゃんと動いてることを確認するために適当にリクエストを飛ばす。
下記の通り返ってくれば終了。

$ curl localhost:8080/
> Hello World!

まとめ

とりあえずメッセージが返ってくるところまで作った。
次はDocker環境で試したり、API増やしたり、HTML返したりできるようにしたいところ。
それでは今回はこの辺で。

おまけ

JavaとかGradleとか大昔に入れてたから、どこまで必要かわからないけどとりあえずインストールメモ

# とりあえずアップデート
$ brew update
# Javaインストール(OpenJDKのv11がインストールされる)
$ brew cask install java
# Gradleインストール
$ brew install gradle
# インストール確認
$ java -version
$ gradle -v

参考資料

clasp + typescriptでAPIを作ってみる

はじめに

GoogleAppsScriptがほぼjsなのにes6でかけなかったり、
tsで書けなかったりちょっと残念だなとか思ってたら公式でサポートされてた。
今回はclaspを使ってtypescriptで書いてAPIを公開してみる。

TL;DR.

今回作ったサンプルコード

インストールする

$ npm install --save-dev @google/clasp
$ clasp -v 
>1.7.0

インストールはこれで完了。
globalにインストールしない場合はフォルダ構成に合わせてpathを通してください。
macならbash_profileに下記追記でOK。

export PATH=$PATH:./node_modules/.bin

プロジェクト作成

# ブラウザが開くと思うので任意のGoogleアカウントでログイン
$ clasp login 

# ソース置いとくフォルダ作成
$ mkdir src

# rootDirを"src"に指定して"sample"という名前のプロジェクト作成
$ clasp create --title "sample" --rootDir ./src

# どれか聞かれるから好みのものを選ぶ(今回はapi)
# APIはデフォルトだとエラーになるので下記URLから事前に「オンにしておく」
# https://script.google.com/home/usersettings
>? Clone which script?
>  standalone
>  docs
>  sheets
>  slides
>  forms
>  webapp
>❯ api

# サンプルのプログラムを作成
# claspではES6もTypeScriptも問題ない
$ touch src/sample.ts

これでプロジェクトの作成は問題ないはず。
サンプルプログラムは下記の通りにした。
(と言っても公式のサンプル丸コピだけど)

// Optional Types
let isDone: boolean = false;
let height: number = 6;
let bob: string = "bob";
let list1: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];
enum Color {Red, Green, Blue};
let c: Color = Color.Green;
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
function showMessage(data: string): void { // Void
 console.log(data);
}
showMessage('hello');

// Classes
class Hamburger {
  constructor() {
    // This is the constructor.
  }
  listToppings() {
    // This is a method.
  }
}

// Template strings
var name = 'Sam';
var age = 42;
console.log(`hello my name is ${name}, and I am ${age} years old`);

// Rest arguments
const add = (a, b) => a + b;
let args = [3, 5];
add(...args); // same as `add(args[0], args[1])`, or `add.apply(null, args)`

// Spread operator (array)
let cde = ['c', 'd', 'e'];
let scale = ['a', 'b', ...cde, 'f', 'g'];  // ['a', 'b', 'c', 'd', 'e', 'f', 'g']

// Spread operator (map)
let mapABC  = { a: 5, b: 6, c: 3};
let mapABCD = { ...mapABC, d: 7};  // { a: 5, b: 6, c: 3, d: 7 }

// Destructure map
let jane = { firstName: 'Jane', lastName: 'Doe'};
let john = { firstName: 'John', lastName: 'Doe', middleName: 'Smith' }
function sayName({firstName, lastName, middleName = 'N/A'}) {
  console.log(`Hello ${firstName} ${middleName} ${lastName}`)  
}
sayName(jane) // -> Hello Jane N/A Doe
sayName(john) // -> Helo John Smith Doe

// Export (The export keyword is ignored)
export const pi = 3.141592;

// Google Apps Script Services
var doc = DocumentApp.create('Hello, world!');
doc.getBody().appendParagraph('This document was created by Google Apps Script.');

// Decorators
function Override(label: string) {
  return function (target: any, key: string) {
    Object.defineProperty(target, key, { 
      configurable: false,
      get: () => label
    });
  }
}
class Test {
  @Override('test') // invokes Override, which returns the decorator
  name: string = 'pat';
}
let t = new Test();
console.log(t.name); // 'test'

pushしてみる

# rootDirに指定したフォルダの内容を全てpush
$ clasp push
└─ src/appsscript.json
└─ src/sample.ts
Pushed 2 files.

pushされたprojectを確認すると下記のようなgsに変化される。

var exports = exports || {};
var module = module || { exports: exports };
var __assign = (this && this.__assign) || Object.assign || function(t) {
    for (var s, i = 1, n = arguments.length; i < n; i++) {
        s = arguments[i];
        for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
            t[p] = s[p];
    }
    return t;
};
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};

// 型定義
var isDone = false;
var height = 6;
var bob = "bob";
var list1 = [1, 2, 3];
var list2 = [1, 2, 3];
var Color;
(function (Color) {
    Color[Color["Red"] = 0] = "Red";
    Color[Color["Green"] = 1] = "Green";
    Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));
var c = Color.Green;
var notSure = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
function showMessage(data) {
    // Void
    Logger.log(data);
}
showMessage("hello");
// クラス
var Hamburger = /** @class */ (function () {
    function Hamburger() {
        // コンストラクタ
    }
    Hamburger.prototype.listToppings = function () {
        // メソッド
    };
    return Hamburger;
}());
// テンプレート文字列
var name = "Sam";
var age = 42;
console.log("hello my name is " + name + ", and I am " + age + " years old");
// Rest arguments
var add = function (a, b) { return a + b; };
var args = [3, 5];
add.apply(void 0, args); // same as `add(args[0], args[1])`, or `add.apply(null, args)`
// スプレッド構文 (array)
var cde = ["c", "d", "e"];
var scale = ["a", "b"].concat(cde, ["f", "g"]); // ['a', 'b', 'c', 'd', 'e', 'f', 'g']
// スプレッド構文 (map)
var mapABC = { a: 5, b: 6, c: 3 };
var mapABCD = __assign({}, mapABC, { d: 7 }); // { a: 5, b: 6, c: 3, d: 7 }
// 分割代入
var jane = { firstName: "Jane", lastName: "Doe" };
var john = { firstName: "John", lastName: "Doe", middleName: "Smith" };
function sayName(_a) {
    var firstName = _a.firstName, lastName = _a.lastName, _b = _a.middleName, middleName = _b === void 0 ? "N/A" : _b;
    console.log("Hello " + firstName + " " + middleName + " " + lastName);
}
sayName(jane); // -> Hello Jane N/A Doe
sayName(john); // -> Helo John Smith Doe
// Export (The export keyword is ignored)
exports.pi = 3.141592;
// Google Apps Script の独自サービスの利用
var doc = DocumentApp.create("Hello, world!");
doc
    .getBody()
    .appendParagraph("This document was created by Google Apps Script.");
// デコレータ(高階関数)
function Override(label) {
    return function (target, key) {
        Object.defineProperty(target, key, {
            configurable: false,
            get: function () { return label; }
        });
    };
}
var Test = /** @class */ (function () {
    function Test() {
        this.name = "pat";
    }
    __decorate([
        Override("test") // invokes Override, which returns the decorator
    ], Test.prototype, "name");
    return Test;
}());
var t = new Test();
console.log(t.name); // 'test'

APIを作ってみる

とりあえず今回は外部から叩けることを確認する。
src/api.tsとしてファイルを作った。
中身は下記の通り。ただ単に適当なJSONを返した。

// doGetはGETで叩かれたときに実行される関数
function doGet() {
    const resData = JSON.stringify({ message: 'Hello World!' });
    // ContentServiceを利用して、responseを作成
    // そのまま返してもエラーになる
    ContentService.createTextOutput();
    const output = ContentService.createTextOutput();
    output.setMimeType(ContentService.MimeType.JSON);
    output.setContent(resData);
    // return response-data
    return output;
}

APIを使えるように設定する

「公開 > 実行可能APIとして導入」からAPIは初公開するとき警告出るけどここではスルーして続行。
スルーするとAPIIDが出てくるのでコピっておく。
スクリプトにアクセスできるユーザは「全員」にしておく。

次に「公開 > ウェブアプリケーションとして導入」から設定する。
プロジェクトバージョンは先程公開したAPIのバージョン、
次のユーザーとしてアプリケーションを実行は「自分」、
アプリケーションにアクセスできるユーザーは「全員(匿名ユーザを含む)」にして導入。
導入すると「現在のウェブ アプリケーションの URL」が出てくるからコピっておく。

APIを叩いてみる

ブラウザから直接叩くかcurl -L 【コピったURL】でOK。
{"message":"Hello World!"}みたいなレスポンスが帰ってくれば成功。

まとめ

今回はclasp + typescriptでGoogle Apps ScriptのAPIを作ってみた。
見た目はtsだったりするけど、中身はGASなのでちゃんとスプレッドシートとかGmailとかと連携はできるはず。
Googleのなんかを使うのはまたいずれ。
それでは今回はこの辺で。

Webpackのビルドをフォルダ構成を変えずに行う

TL;DR.

今回作ったサンプル

はじめに

前回Webpackでビルドするところまでやったけど、
やっぱりファイルまとめたくなかったので色々したのが今回。

準備

前回の状態にしておく。
ちなみにフォルダ構成はこんな感じ。(tssrver→srcにしました。)

.
├── node_modules
├── package-lock.json
├── package.json
└── src
     ├── app.ts
     ├── bin
     │   └── www.ts
     ├── public
     │   ├── images
     │   ├── javascripts
     │   └── stylesheets
     │       └── style.css
     ├── routes
     │   ├── index.ts
     │   └── users.ts
     └── views
         ├── error.jade
         ├── index.jade
         └── layout.jade

webpack.config.jsを修正

各種ファイルを纏めたくは無いので、entryを分けておく。
ついでにフォルダ構成をそのままにしたいので、keyに出力後のファイル名、valueに元ファイルを記載する。

const path = require('path');

module.exports = {
  mode: 'development',
  entry: {
      'bin/www': path.join(__dirname, 'src/bin/www.ts'),
      'routes/index.js': path.join(__dirname, 'src/routes/index.ts'),
      'routes/users.js': path.join(__dirname, 'src/routes/users.ts'),
      'app.js': path.join(__dirname, 'src/app.ts')
  },
  target: 'node',
  node: {
    // ビルド後の位置を参照したいのでfalseにする
    __dirname: false
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader'
      }
    ]
  },
  resolve: {
    modules: [
      'node_modules'
    ],
    extensions: [
      '.ts', '.js'
    ]
  },
  output: {
    // 出力するファイル名(entryのkeyを指定)
    filename: '[name]',
    path: path.join(__dirname, 'dist')
  }
};

ビルド結果後のフォルダ構成はこんな感じ。
一部置いてかれているけど、今回は無視する。
必要に応じて別途コピーすれば問題ないと思う。

.
├── dist
│   ├── app.js
│   ├── bin
│   │   └── www
│   └── routes
│       ├── index.js
│       └── users.js
├── node_modules
├── package-lock.json
├── package.json
└── src
     ├── app.ts
     ├── bin
     │   └── www.ts
     ├── public
     │   ├── images
     │   ├── javascripts
     │   └── stylesheets
     │       └── style.css
     ├── routes
     │   ├── index.ts
     │   └── users.ts
     └── views
         ├── error.jade
         ├── index.jade
         └── layout.jade

まとめ

ようやく分けてビルドすることができた。
ただ、ファイルが増える度にconfigを書き換えなきゃ行けないので、そのうち何か考えたいです。

Vue.jsとfirebaseでサーバーレスを試してみる

はじめに

最近何かとよく聞くサーバレス。
firebaseで簡単にできるみたいな記事をよく見るので今回は自分でやってみる。
構成はfirebase + Vue.jsでやる。
内容としては超絶簡単なプロフィールみたいなものを作る。

TL;DR.

https://mypage-e6eae.firebaseapp.com/

前提

Nodeが入っている。
Googleアカウントがある。

firebaseの設定

まずはコンソールにアクセスしてプロジェクトを作っておく。
名前等は何でもOK。

プロジェクトを作れたらnpm install -g firebase-toolsCLIをインストール。
インストールできたら下記手順で設定する。

$ firebase login
> #ブラウザが立ち上がるので、任意のGoogleアカウントにログイン
$ firebase init
# ホスティングで使いたいので、Hostingにチェック
? Which Firebase CLI features do you want to setup for this folder? Press Space to select features, then Enter to confirm your choices.
 ◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
 ◯ Functions: Configure and deploy Cloud Functions
❯◯ Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules

# deployするアプリ(今回だとVueのビルド結果)のディレクトリpublicにするか聞かれる
What do you want to use as your public directory? 
# SPAにするか聞かれる
Configure as a single-page app (rewrite all urls to /index.html)?
# index.htmlあるけど、上書きするか(ファイルある場合。無いと勝手に生成される)
File public/index.html already exists. Overwrite? 

これで設定は問題ないはず。

Vueの設定

VueCLIで作ったプロジェクトをそのまま使う。
具体的なVueの設定は前にやったので、そちらを参照してください。

コードは一部内容をプロフィールっぽくするのとfirebaseの設定を記載。

f:id:minase_mira:20181219231253p:plain

画像みたいなアイコンをクリックするとウェブアプリにfirebaseを追加みたいなポップアップが出てくる。
// Initialize Firebaseって書いてあるconfigmain.tsに追記する。

ビルドの出力先を変えたいのでnpm run build -- --dest publicで実行。
buildの内容はデフォルトのまま。

デプロイしてみる

firebase deployでOK。
デプロイが完了するとURLが表示されるのでそこにアクセスする。

$ firebase deploy
=== Deploying to 'mypage-e6eae'...

i  deploying hosting
i  hosting[mypage-e6eae]: beginning deploy...
i  hosting[mypage-e6eae]: found 14 files in public
✔  hosting[mypage-e6eae]: file upload complete
i  hosting[mypage-e6eae]: finalizing version...
✔  hosting[mypage-e6eae]: version finalized
i  hosting[mypage-e6eae]: releasing new version...
✔  hosting[mypage-e6eae]: release complete

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/mypage-e6eae/overview
Hosting URL: https://mypage-e6eae.firebaseapp.com

f:id:minase_mira:20181219231408p:plain

まとめ

今回はVue + firebaseでサーバレスを試してみた。
firebaseが思ってたより簡単だし、無料で使えるしとっつきやすいと思った。
そのうちこのプロフィールページも充実させていこう……

参考サイト

SCSSまとめ

はじめに

CSS書く時にいちいち調べながら書いてたけど、いい加減にちゃんと勉強しようと思った今日この頃。
素のCSSを書くのも良いかも知れないけど、せっかくなのでSass(SCSS)で書けるようにまとめる。

Sass(SCSS)とは

CSSメタ言語
SassとSCSSの違いは書き方の違いのみ。
CSSっぽく書いたSassがSCSSといった具合。
同じものなので、どちらの記法で書いたとしても同じCSSが生成される。
以降のサンプルは全部SCSSで書いた場合。

SCSSの機能一覧

機能1: 変数

SCSSでは変数が使える。
$変数名: 値で変数を定義できる。
色とか共通のものあるだろうし、一括定義できるのは便利。

$red = #FF3333;
.text {
    color: $red;
}

機能2: ネスト

HTMLみたくネストして書くことができる。
元のCSSでも一応ネストしてかけるけど、
入れ子でかけるほうがわかりやすいと思う。

div {
    ul {
        background-color: #000;
        li {
            color: #fff;
        }
    }
}

// ちなみに普通のCSSでは下記みたいに書く
div ul {
    background-color;
}
div ul li {
    color: #fff;
}

機能3: パーシャル化

1つのCSSを書く際に機能毎に分割することができる。
分割されたファイル名を_ファイル名.scssの様にすることによってCSSに変換する際に変換対象外にできる。
簡単に言うとコイツは別のとこで読み込むから変換しないでファイル。
分割されたファイルは使いたいSCSSで@import 'ファイル名'でimportして使う。(_はなくていい)

// _importedFile.scss
div {
    color: #000;
}

// main.scss
// importして使う
@import 'importedFile';
span {
    color:#fff;
}

機能4: Import

上の通り。
別に_付きのファイルじゃなくてもimportはできる。
ちなみに素のCSSにもimport機能はあるけど、読み込む度にHTTPリクエストが発生する欠点があった。
SCSSでのimportは読み込んだ上で単一のCSSとして生成されるのでリクエストが発生するデメリットはない。

機能5: Mixin

サイト内で使いまわしたい宣言を使い回せるよっていう機能。
使い回される宣言に@mixinと付ければ使い回せるようになる。
使う際には@incliudeを付けてることで@mixinで宣言した内容を呼び出せる。
ちなみに@mixin(変数)とすることで変数を受け取ることもできる。優秀。

@mixin transform($property) {
  -webkit-transform: $property;
  -ms-transform: $property;
  transform: $property;
}
@mixin box_define($width:50px, $height:50px, $bg_color:white) {
  width: $width;
  height: $height;
  background: $bg_color;
}

.box { @include transform(rotate(30deg)); }
.box2 {
  /* 引数には初期値も設定できる。省略するとそれが使われる */
  @include box_define();
}

機能6: Extend/Inheritance

@extendを使うことであるセレクタのプロパティを別のセレクタに共有できる。
HTMLに複数のクラス名を記述する必要がなくなるとかメリットがある。

/* この定義を他のセレクタで使い回せる。 */
%message-shared {
  border: 1px solid #ccc;
  padding: 10px;
  color: #333;
}

.message {
  @extend %message-shared;
}

.success {
  @extend %message-shared;
  border-color: green;
}

.error {
  @extend %message-shared;
  border-color: red;
}

.warning {
  @extend %message-shared;
  border-color: yellow;
}

機能7: 四則演算

+,-,*,/,%みたいなよくある四則演算が使える。
内容もよくある四則演算なので割愛。

作ったサンプル

ここ
ちなみに全部は使ってない。

まとめ

素のCSS書くのやめようと思った。
今回使えなかったものも組み込んで使えるようにしたい。

参考サイト

NgRx使ってみる

TL;DR.

ソース

はじめに

Angularでアプリを作っていると状態管理というかデータの取り回しについて困ることが多々あったりする。
そこでいろいろ調べた結果やっぱりRedux最強説がっぽい。
自力でReduxチックに実装したこともあるけど、
既にAngularに最適化されたライブラリがあったからそれを使おうというのが今回のテーマ。

ngrxとは

端的に言うとAngular向けRedux。
Angularに向けてRxJS使って設計されているって公式サイトに書いてあった。

インストール

# 状態管理ライブラリメインとなるモジュール
npm install @ngrx/store

サンプル作成

公式サイトのCounterを参考にTODOアプリを作ってみる。
ng new ngrx-sampleからスタート。
必要なファイルを先に作っておく。

# action
$ mkdir src/app/action && touch src/app/action/todo.action.ts
# reducer
$ mkdir src/app/reducer && touch src/app/reducer/todo.reducer.ts

action

基本的にはチュートリアルのまま。
ただ、今回は任意の値が欲しかったから、追加時にはconstructorで値を受け取っておく。

import { Action } from '@ngrx/store';

export enum ActionTypes {
    Add = 'TODO_ADD',
    Delete = 'TODO_DELETE',
    Reset = 'TODO_RESET'
}

export class Add implements Action {
    readonly type = ActionTypes.Add;
    // 追加したい文字列を受け取る
    constructor(public payload: { text: string }) { }
}
export class Delete implements Action {
    readonly type = ActionTypes.Delete;
}
export class Reset implements Action {
    readonly type = ActionTypes.Reset;
}

reducer

これも基本的にはチュートr(ry
一応副作用が無いように追加の時にはスプレッド演算子使って、削除の時にはフィルター使った。
それ以外は基本そのまま。

import { Action } from '@ngrx/store';
import { ActionTypes } from '../action/todo.action';

export const initialList: Array<string> = [];

export function todoReducer(state = initialList, action: Action): Array<string> {
    switch (action.type) {
        case ActionTypes.Add:
            // 末尾に新規のものを追加
            // state.push(action['payload']['text'])だとstateが変わってしまうのでNG
            return [...state, action['payload']['text']];
        case ActionTypes.Delete:
            // 末尾を削除
            // state.pop()だとstateが変わってしまうのでNG
            return state.filter((value, index) => index !== (state.length - 1));
        case ActionTypes.Reset:
            return [];
        default:
            return state;
    }
}

app.module.ts

名前が違うくらい。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

import { StoreModule } from '@ngrx/store';
import { todoReducer } from './reducer/todo.reducer';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    StoreModule.forRoot({ todo: todoReducer })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

UI

TODO登録するUI。
テキストボックスと「追加」、「削除」、「リセット」の3ボタンを置いただけ。
追加したTODOはリスト表示する。

app.component.html

<input type="text" #text><br>
<button type="button" (click)="add(text.value)">Add</button>
<button type="button" (click)="delete()">Delete</button>
<button type="button" (click)="reset()">Reset</button>
<ul>
  <li *ngFor="let todo of todolist">{{todo}}</li>
</ul>

app.component.ts

import { Component } from '@angular/core';

import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { Add, Delete, Reset } from './action/todo.action';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  public todo$: Observable<Array<string>>;
  public todolist: Array<string> = [];

  constructor(private store: Store<{ todo: Array<string> }>) {
    this.todo$ = store.pipe(select('todo'));
    this.todo$.subscribe(res => this.todolist = res);
  }

  public add(value: string) {
    this.store.dispatch(new Add({ text: value }));
  }
  public delete() {
    this.store.dispatch(new Delete());
  }
  public reset() {
    this.store.dispatch(new Reset());
  }
}

まとめ

今回使ったのはNgRxが提供してるライブラリの中でも一番とっつきやすいところをやってみた。
APIコールとか副作用があるものに向けた@ngrx/effectsとかもあるからそのうち触ってみる。

参考サイト