Vue と typeless を組み合わせるために Vue 用に React Hooks を作る

 
# 経緯

React 用に作られた状態管理ライブラリ typeless が良さげだったのですが
最近使っているのが Vue なので Vue と typeless を組み合わせたいなと思いました。

# 完成品

[github:ytoune/practice-20191019-typeless-with-vue-hooks](https://github.com/ytoune/practice-20191019-typeless-with-vue-hooks)
[動作例](https://ytoune.github.io/practice-20191019-typeless-with-vue-hooks)

### hooks を使う

```vue:src/comps/CountUp.vue
<template>
	<div>
		<p>count: {{ count }}</p>
		<p><button @click="countUp">+1</button></p>
	</div>
</template>
<script lang="ts">
import Vue from 'vue'
import { Component, Prop } from 'vue-property-decorator'
import { useActions } from 'typeless'
import { useModule, CounterActions, getCounterState } from '~/store'

@Component({
	hooks() {
		useModule()
		const acts = useActions(CounterActions)
		const state = getCounterState.useState()
		const count = state.count
		const countUp = acts.startCount
		return { count, countUp }
	},
})
export default class CountUp extends Vue {
	count!: number
	countUp!: Function
}
</script>
```

# 問題点

typeless は React の hooks に依存しています。
具体的にはソースコードに `import * as React from 'react'` と書かれています。

今回は Vue 用に React の API を再現します。

# ディレクトリ構造

```
- workspace
  |- src
  |  |- index.ts
  |  |- App.vue
     # その他省略
  |- package.json
  |- tsconfig.json
  # その他省略
```

# パスの設定

TypeScript と Parcel のパスの設定を行います。
（今回は Parcel を使っています）

```js:tsconfig.json
{
  "compilerOptions": {
    /* 略 */
    "paths": {
      "~/*": ["*"],
      "react": ["src/react/core.ts"],
      "react-dom": ["src/react/dom.ts"]
    },
    /* 略 */
  },
  /* 略 */
}
```

```js:package.json
{
  /* 略 */
  "alias": {
    "react": "./src/react/core.ts",
    "react-dom": "./src/react/dom.ts"
  }
  /* 略 */
}
```

# Vue 用に React の API を再現する

実装優先でパフォーマンスなどを無視しているところが多いです。

### 既存のライブラリ

できる限り既存の遺産を再利用したいですね。

* [github:yyx990803/vue-hooks](https://github.com/yyx990803/vue-hooks)

  Vue フレームワークの創始者である Evan You さんが似たようものを作り始めているみたいです。
    `useState` などの一部の hooks が実装されています。
  
* [github:TotooriaHyperion/vue-hooks](https://github.com/TotooriaHyperion/vue-hooks)

  上記の fork です。
    `useMemo` などが追加されています。

* [github:zephraph/vue-context-api](https://github.com/zephraph/vue-context-api)

  `createContext` や `Provider` などの React の ContextAPI の Vue 向け実装です。
    ただし `useContext` がないです。

### 不足している実装を埋める

#### useReducer

```ts:src/react/hooks.ts
export const useReducer = (reducer: any, initialState: any) => {
	const [s, update] = useState(initialState)
	const dispatch = (action: any) => {
		update(reducer(s, action))
	}
	return [s, dispatch]
}
```

#### useLayoutEffect

とりあえず useEffect を複製してます。

```ts:src/react/hooks.ts
export const useLayoutEffect = useEffect
```

#### useContext

vue-context-api では、値を context.value に代入しています。
これを useContext で取り出します。

```diff
+const getcontext = '#context'
export const createContext = (defaultValue: any) => {
	const context = {}
	return {
+		[getcontext]: context,
		Provider: Provider(context, defaultValue),
		// Consumer: Consumer(context)
		Consumer: null,
	}
}
```

```ts:src/react/hooks.ts
export function useContext(context: any) {
	return context[getcontext].value
}
```

#### unstable_batchedUpdates

すべてのインスタンスを `$forceUpdate` してます。
ゴリ押しです。

```ts:src/react/dom.ts
export const unstable_batchedUpdates = (fn: () => void) => {
	fn()
	for (const ins of list) ins.$forceUpdate()
}
export const batchedUpdates = unstable_batchedUpdates

const list: any[] = []

Vue.use(function(Vue: any) {
	Vue.mixin({
		beforeCreate() {
			list.push(this)
		},
		beforeDestroy() {
			const i = list.indexOf(this)
			if (~i) {
				list.splice(i, 1)
			}
		},
	})
})
```

とりあえず上記を実装でも動くみたいです。