Hyperapp V2

Hyperapp V2がリリースされました。:tada:

~~2019/03/27 追記：alpha.10に合わせて全面的に書き換えました。（おそらく次のアップデートでイベント名が小文字になる予定です。）~~
2019/09/14 追記：v2.0.1に合わせて書き換え。
2020/07/15 追記：[v2.0.5](https://github.com/jorgebucaran/hyperapp/pull/953)がリリースされました。ESモジュール化、JSXの直接的なサポート・Payload Creatorが削除、`Lazy`が`memo`にリネームされています。（**本記事に未反映**）

# Hyperappとは

VDOMを搭載した超絶小さい（ソースが読める）JavaScriptフレームワークです。（2KBぐらい）

[詳しくはこちら](https://qiita.com/JorgeBucaran/items/c48446babe0627e25ee6#1-kb-%E3%81%AB%E3%81%93%E3%81%A0%E3%82%8F%E3%81%A3%E3%81%9F%E7%90%86%E7%94%B11-kb-%E3%81%A7%E3%81%82%E3%82%8B%E3%81%93%E3%81%A8%E3%81%AE%E5%88%A9%E7%82%B9)

# Hyperapp V2

リファレンス

* V2のREADME https://github.com/jorgebucaran/hyperapp/blob/master/README.md
* V2マイルストーン https://github.com/jorgebucaran/hyperapp/milestone/7
* V2のプルリク https://github.com/jorgebucaran/hyperapp/pull/726
* Action API https://github.com/jorgebucaran/hyperapp/issues/749
* Effects API https://github.com/jorgebucaran/hyperapp/issues/750
* Subscriptions API https://github.com/jorgebucaran/hyperapp/issues/752
* class/className attribute API https://github.com/jorgebucaran/hyperapp/issues/754
* Lazy Views https://github.com/jorgebucaran/hyperapp/pull/777
* Middleware API https://github.com/jorgebucaran/hyperapp/issues/753

## V2概略図
![https://github.com/diontools/hyperapp-learn/raw/master/diag.png](https://github.com/diontools/hyperapp-learn/raw/master/diag.png)

* Actions
  * Stateの更新
  * Effectの作成
* Effects
  * 非同期処理 - HTTP要求とか
  * 副作用 - URLの変更(history API)とか
* Subscriptions
  * グローバルイベントの管理 - Windowイベントとか
  * カスタムイベントストリームの管理 - Timer(setInterval)とか

## V1からの変更点

* State - Action - View でセットだったV1から**Actionが分離**されました（Actionは純粋な関数に変更）
  * ActionからActionを呼び出せなくなりました
* Actionが分離されたため**Nested Actionが消滅**しました
* パラメーター付きのActionを呼び出すときは**タプル**を使います
* 非同期Actionの代わりに**Effectが追加**されました
* イベントの購読とその解除のための**Subscriptionが追加**されました
* **ライフサイクルイベントが削除**されました（[対応策](https://github.com/jorgebucaran/hyperapp/issues/717)）
* SVGの**xlink属性のサポートが削除**されました（[詳細](https://github.com/jorgebucaran/hyperapp/pull/726#issuecomment-472254112)）
* DOM更新アルゴリズムが変更され**高速化**しました https://github.com/jorgebucaran/hyperapp/issues/499#issuecomment-403257843
* ~~renderを実行するタイミングが変更され、待ち時間(setTimeout)なしで実行することで**高速化**しました~~
  * 問題があったため[**requestAnimationFrameを使用**するように変更](https://github.com/jorgebucaran/hyperapp/commit/c7ae927d4fefb4f125a27ce61aec9894445c74be)されました

## 動くサンプルアプリ

数値をカウントするだけのアプリです。JSXで書かれています。

[![Edit hyperapp v2 alpha](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/embed/hyperapp-v2-vgpif?fontsize=14&module=%2Fsrc%2Findex.js)

## インストール

```shell
npm i hyperapp
```

JSXを使用する場合はbabelのtransform-react-jsxでpragmaにhを指定します。

## 公開している関数

Hyperappは3つの関数を公開(export)しています。

* `h`関数
  * VDOMのノード(**VNode**)を作成する関数
  * JSXを使用して間接的に利用します（直接使ってもOK:ok_hand:）
* `app`関数
  * Hyperappアプリのエントリポイント
  * 詳細は後述
* `Lazy`関数
  * 後述のLazy Viewで使用するVNodeを作成する関数

## Actions

**Action**は新しいStateとEffectの作成を行う関数です。次のように書きます。

```jsx
// Stateのvalueに1を設定するAction
const Foo = state => ({ ...state, value: 1 });

// Payload付きAction
const Foo = (state, payload) => ({ ...state, value: payload.value });
```

:warning:注意：引数のStateを変更してはいけません。Stateを更新する場合は、常に新しいStateを作成する必要があります。

**Payload**はどこからActionが呼び出されるのかによって中身が決まります(後述の`dispatch`関数を参照)。VDOMのイベントから呼び出される場合は[Event](https://developer.mozilla.org/ja/docs/Web/API/Event)が設定されます。

Stateを更新したくない場合は引数のStateをそのまま返せばよいでしょう。

```jsx
const Foo = state => state;
```

Actionの戻り値は上記のようにStateを返すほか、後述のEffectを返すパターンもあります。

```jsx
const Foo = state => [ nextState, nextEffect ];
```

Effectは続けて複数指定できます。

```jsx
const Foo = state => [ nextState, nextEffect1, nextEffect2, ... ];
```

## Effects

**Effect**は非同期処理や副作用を扱います。簡単に言うと、Stateを変更しない動作を行います。

```jsx
// 指定した時間後に指定したActionを呼び出すEffect Runner
const delayRunner = (dispatch, { action, interval }) => {
  setTimeout(() => dispatch(action, 'delay!'), interval);
};

// delayRunnerを実行するEffectを作成するEffect Constructor
const delay = (action, { interval }) => [
  delayRunner,
  { action, interval }
];

// delayから呼び出されるAction （最終的にtextへ'delay!'が設定される）
const Delayed = (state, payload) => ({ ...state, text: payload });

// Effectのdelayを呼び出すAction （1000ms後にDelaydを呼び出す）
const DelayWithAction = state => [
    state,
    delay(Delayed, { interval: 1000 })
];
```

`delay`がEffectを作成する関数（**Effect Constructor**と呼ばれる）です。

Effectは関数とパラメーターのタプル（配列）であり、関数は**Effect Runner**と呼ばれています。

```javascript
const effect = [effectRunner, parameters]
```

Effect Runnerでは、第1引数に`delay`で作成したパラメーターがそのまま入り、第2引数にActionを実行する`dispatch`関数が入ります。

## `dispatch`関数

**`dispatch`**関数は、Actionを実行し、作成されたStateの更新とEffectの実行を行う関数です。おもに第1引数はAction、第2引数はActionパラメーターを渡します。

```jsx
// ActionとPayload
dispatch(action, payload);

// タプルなPayload付きAction
dispatch([action, payload]);

// Payload Creator付きAction（Effect/Subscription内部やVDOMのイベントで使用）
dispatch([action, payloadCreator], payloadCreatorParameter);

// StateとEffect（Actionの戻り値と同様）
dispatch([newState, newEffect]);

// State（Actionの戻り値と同様）
dispatch(newState);
```

>  Actionを`dispatch`する理由は、ロギングなどで関数名を取得するためです。Actionを自分で呼び出してしまうと`dispatch`内部において`function.name`で関数名（Actionの名前）を取得できなくなります。将来、Hyperappのデバッグツールが作られるときに必要になります。

## `app`関数

`app`関数でHyperappのアプリを開始します。

```jsx
app({
  init: { value: 0 },
  view: state => <div>{state.value}</div>,
  subscriptions: state => ...,
  node: document.body,
  middleware: ...,
});
```

## `init`プロパティ

**`init`**プロパティには初期化に使うオブジェクトを指定します。

**`init`**プロパティに指定されたオブジェクトはそのまま`dispatch`関数に渡されます。そのため、前述の**`dispatch`**関数で使用できるActionやEffectも使用できます。

```jsx
init: { foo: 'bar' },
init: InitAction,
init: [InitAction, { foo: 'bar' }],
init: [{ foo: 'bar' }, delay(...)],
```

## `node`プロパティ

**`node`**プロパティにはレンダリング先の要素（Element）を指定します。

## `view`プロパティ

**`view`**プロパティには、VNodeを作成する関数を指定します。引数にStateが渡されます。ボタンのクリックでActionを発生させる場合は次のように書きます。

```jsx
app({
  view: state => <button onClick={action}>{state.value}</button>
});
```

JSXを使わずに書くと次のようになります。

```js
app({
  view: state => h("button", { onClick: action }, state.value)
});
```

小文字の**on**から始まる属性は、`dispatch`関数の呼び出しに変換され、`addEventListener`で登録されます。（内部で小文字に正規化されます）

また、`dispatch`されるときデータにEventオブジェクトが設定されます。`onClick`はClickイベントに`event => dispatch(action, event)`が登録されます。

イベント属性に指定した値は`dispatch`に渡されるため、次のような書き方も可能です。

```jsx
onClick={[action, payload]}
onClick={[action, payloadCreator]} // 後述
onClick={[newState, newEffect]}
onClick={newState}
```

#### Payload Creator
ActionのPayloadを作成する関数を**Payload Creator**と呼びます。VDOMイベントに次のように指定すると、第1引数にEventが渡されます。

```jsx
onClick={[action, ev => ({ value: ev.currentTarget.value })]}
```

Payload Creatorの戻り値が、指定したActionのPayloadになります。上記の場合、Payloadは`{ value }`です。

`preventDefault()`をここで呼び出すこともできます。

```jsx
onSubmit={[action, ev => ev.preventDefault()]}
```

Payloadは`undefined`（`preventDefault()`の戻り値）になるので、Payloadを指定したい場合は、次のように書きます。

```jsx
onSubmit={[action, ev => { ev.preventDafault(); return { value: 1 } }]}

// もしくはヘルパー関数を作って...
const preventDefault = (action, payload) => [action, ev => { ev.preventDefault(); return payload }]
onSubmit={preventDefault(action, { value: 1 })}
```

### `key`

VNodeに**`key`**プロパティを指定すると、更新時に同じDOMノードが使いまわされます。

```jsx
h('div', { key: 'foo' });
<div key="foo" />
```

### `class`

`class`プロパティで[classcat](https://github.com/jorgebucaran/classcat)ライクに[className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className)を記述することができます。

```jsx
// <div class="foo" />
<div class={{ foo: true, bar: false }} />
<div class={['foo', { bar: false }]} />
<div class={'foo'} />
```

### `style`

**`style`**プロパティでインラインスタイルを記述することができます。

```jsx
// <div style="border: solid 1px red" />
<div style={{ border: 'solid 1px red' }} />
```

## `subscriptions` プロパティ

**`subscriptions`**プロパティには、**Subscription**オブジェクトの配列を返す関数を指定します。第一引数にStateが渡されます。省略可能。

この関数はStateが変更されるたびに呼び出され、Effectに似た**Subscription**オブジェクトを返します。Effectとの唯一の違いは、**Effect Runner**で「購読を解除する関数」を返すところです。

```jsx
// 指定した時間間隔で指定したActionを定期的に呼び出すEffect Runner
const tickRunner = (dispatch, { action, interval }) => {
  const id = setInterval(() => dispatch(action), interval);
  return () => clearInterval(id); // 購読解除用関数
};

// tickRunnerを実行するSubscriptionを作成するSubscription Constructor
const tick = (action, { interval }) => [
  tickRunner,
  { action, interval }
];

// tickで呼び出されるAction
const action = state => ({ ...state, value: state.value + 1 });

app({
  // valueが1のときにtickを購読
  subscriptions: state => [
      state.enabled && tick(action, { interval: 1000 })
  ],
});
```

上記の例ではStateの`enabled`が`true`のとき`tick`を購読します。`enabled`が`false`になると`tick`が購読解除されます。

:warning:注意：**Subscription**を定義する場合、`effect`プロパティに指定する**Effect Runner**は別変数に格納しておきましょう。匿名関数を使用すると正しく購読が継続されません。`effect`の値が別オブジェクトと認識され、Stateが変化するたびに再講読されてしまいます。

**Subscriptions**は複数の**Subscription**オブジェクトを返せます。

```jsx
app({
  subscriptions: state => [
    tick(...),
    foo(...),
  ],
});
```

## EffectとSubscriptionのライブラリ
基本的なEffectとSubscriptionはhyperapp-fxで定義済みです。
hyperapp-fx
https://github.com/okwolf/hyperapp-fx/tree/HAv2

## `middleware`プロパティ

**`middleware`**プロパティには、次のような関数を指定することで、`dispatch`されるときに処理を挟むことができます。省略可能。

```jsx
app({
  middleware: dispatch => (action, payload) => {
    console.log('dispatching', action, payload);
    dispatch(action, payload);
    console.log('dispatched', action, payload);
  }
})
```

## Lazy View

**Lazy View**は、Stateが変化しないときにVDOMを使いまわすパフォーマンス向上機能です。

VDOMを使いまわすことでDOMに反映させる処理がスキップされますが、Lazy VNodeの浅い比較処理があるため、小さいVDOMだと逆効果。

> また、Lazy ViewはV1のLazy Componentsとはまったく関係ありません。

```jsx
import { h, Lazy, app } from "hyperapp";

// autoが変化したときのみにレンダリングされる部分
const lazyView = props => (
  <p>
    auto:{props.auto ? "enable" : "disable"}
    <br />
    update: {new Date().toISOString()}
  </p>
);

app({
  view: state => (
    // ...
    {Lazy({ view: lazyView, key: "lazy-view", auto: state.auto })}
    // ...
  )
})
```

JSXで書くことも可能です。

```jsx
<Lazy view={lazyView} key="lazy-view" auto={state.auto} />
```

**`Lazy`**関数でLazy View用のVNodeを作ります。**`Lazy`**関数では、`render`プロパティや`key`プロパティなどを持つオブジェクトを渡します。

`render`プロパティには、VNodeを作成する関数を指定します。この関数の引数には**`Lazy`**に指定したオブジェクトが渡されます。

`key`プロパティには、キーとなる文字列を指定します。これはLazy VNodeを効率よく使いまわすための識別子となります（通常のVNodeの`key`と同じ）。必須ではないですが、付けることが推奨されています。

`render`や`key`以外のプロパティを指定することで、指定した値が変化したときに`render`が実行されます。

# おわりに

モジュール化に関してはよくわかっていません。:innocent: