├── .gitignore
├── package.json
├── example3.html
├── example2.html
├── example.html
├── README.md
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | experiment.js
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-hooks",
3 | "version": "0.3.2",
4 | "description": "Experimental React hooks implementation in Vue",
5 | "main": "index.js",
6 | "author": "Evan You",
7 | "license": "MIT"
8 | }
9 |
--------------------------------------------------------------------------------
/example3.html:
--------------------------------------------------------------------------------
1 |
2 |
count is: {{ data.count }}
3 |
double is: {{ double }}
4 |
5 |
6 |
7 |
8 |
36 |
--------------------------------------------------------------------------------
/example2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
53 |
--------------------------------------------------------------------------------
/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
110 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # **!!! ARCHIVED !!!**
2 |
3 | Vue now has its own hooks inspired API for composing logic: https://composition-api.vuejs.org/
4 |
5 | This PoC has completed its purpose and will no longer be updated.
6 |
7 | ---
8 |
9 | # vue-hooks
10 |
11 | POC for using [React Hooks](https://reactjs.org/docs/hooks-intro.html) in Vue. Totally experimental, don't use this in production.
12 |
13 | [Live Demo](https://codesandbox.io/s/jpqo566289)
14 |
15 | ### React-style Hooks
16 |
17 | ``` js
18 | import Vue from "vue"
19 | import { withHooks, useState, useEffect } from "vue-hooks"
20 |
21 | // a custom hook...
22 | function useWindowWidth() {
23 | const [width, setWidth] = useState(window.innerWidth)
24 | const handleResize = () => {
25 | setWidth(window.innerWidth)
26 | };
27 | useEffect(() => {
28 | window.addEventListener("resize", handleResize)
29 | return () => {
30 | window.removeEventListener("resize", handleResize)
31 | }
32 | }, [])
33 | return width
34 | }
35 |
36 | const Foo = withHooks(h => {
37 | // state
38 | const [count, setCount] = useState(0)
39 |
40 | // effect
41 | useEffect(() => {
42 | document.title = "count is " + count
43 | })
44 |
45 | // custom hook
46 | const width = useWindowWidth()
47 |
48 | return h("div", [
49 | h("span", `count is: ${count}`),
50 | h(
51 | "button",
52 | {
53 | on: {
54 | click: () => setCount(count + 1)
55 | }
56 | },
57 | "+"
58 | ),
59 | h("div", `window width is: ${width}`)
60 | ])
61 | })
62 |
63 | // just so you know this is Vue...
64 | new Vue({
65 | el: "#app",
66 | render(h) {
67 | return h("div", [h(Foo), h(Foo)])
68 | }
69 | })
70 | ```
71 |
72 | ### Vue-style Hooks
73 |
74 | API that maps Vue's existing API.
75 |
76 | ``` js
77 | import {
78 | withHooks,
79 | useData,
80 | useComputed,
81 | useWatch,
82 | useMounted,
83 | useUpdated,
84 | useDestroyed
85 | } from "vue-hooks"
86 |
87 | const Foo = withHooks(h => {
88 | const data = useData({
89 | count: 0
90 | })
91 |
92 | const double = useComputed(() => data.count * 2)
93 |
94 | useWatch(() => data.count, (val, prevVal) => {
95 | console.log(`count is: ${val}`)
96 | })
97 |
98 | useMounted(() => {
99 | console.log('mounted!')
100 | })
101 | useUpdated(() => {
102 | console.log('updated!')
103 | })
104 | useDestroyed(() => {
105 | console.log('destroyed!')
106 | })
107 |
108 | return h('div', [
109 | h('div', `count is ${data.count}`),
110 | h('div', `double count is ${double}`),
111 | h('button', { on: { click: () => {
112 | // still got that direct mutation!
113 | data.count++
114 | }}}, 'count++')
115 | ])
116 | })
117 | ```
118 |
119 | ### Usage in Normal Vue Components
120 |
121 | ``` js
122 | import { hooks, useData, useComputed } from 'vue-hooks'
123 |
124 | Vue.use(hooks)
125 |
126 | new Vue({
127 | template: `
128 |
129 | {{ data.count }} {{ double }}
130 |
131 | `,
132 | hooks() {
133 | const data = useData({
134 | count: 0
135 | })
136 |
137 | const double = useComputed(() => data.count * 2)
138 |
139 | return {
140 | data,
141 | double
142 | }
143 | }
144 | })
145 | ```
146 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | let currentInstance = null
2 | let isMounting = false
3 | let callIndex = 0
4 |
5 | function ensureCurrentInstance() {
6 | if (!currentInstance) {
7 | throw new Error(
8 | `invalid hooks call: hooks can only be called in a function passed to withHooks.`
9 | )
10 | }
11 | }
12 |
13 | export function useState(initial) {
14 | ensureCurrentInstance()
15 | const id = ++callIndex
16 | const state = currentInstance.$data._state
17 | const updater = newValue => {
18 | state[id] = newValue
19 | }
20 | if (isMounting) {
21 | currentInstance.$set(state, id, initial)
22 | }
23 | return [state[id], updater]
24 | }
25 |
26 | export function useEffect(rawEffect, deps) {
27 | ensureCurrentInstance()
28 | const id = ++callIndex
29 | if (isMounting) {
30 | const cleanup = () => {
31 | const { current } = cleanup
32 | if (current) {
33 | current()
34 | cleanup.current = null
35 | }
36 | }
37 | const effect = function() {
38 | const { current } = effect
39 | if (current) {
40 | cleanup.current = current.call(this)
41 | effect.current = null
42 | }
43 | }
44 | effect.current = rawEffect
45 |
46 | currentInstance._effectStore[id] = {
47 | effect,
48 | cleanup,
49 | deps
50 | }
51 |
52 | currentInstance.$on('hook:mounted', effect)
53 | currentInstance.$on('hook:destroyed', cleanup)
54 | if (!deps || deps.length > 0) {
55 | currentInstance.$on('hook:updated', effect)
56 | }
57 | } else {
58 | const record = currentInstance._effectStore[id]
59 | const { effect, cleanup, deps: prevDeps = [] } = record
60 | record.deps = deps
61 | if (!deps || deps.some((d, i) => d !== prevDeps[i])) {
62 | cleanup()
63 | effect.current = rawEffect
64 | }
65 | }
66 | }
67 |
68 | export function useRef(initial) {
69 | ensureCurrentInstance()
70 | const id = ++callIndex
71 | const { _refsStore: refs } = currentInstance
72 | return isMounting ? (refs[id] = { current: initial }) : refs[id]
73 | }
74 |
75 | export function useData(initial) {
76 | const id = ++callIndex
77 | const state = currentInstance.$data._state
78 | if (isMounting) {
79 | currentInstance.$set(state, id, initial)
80 | }
81 | return state[id]
82 | }
83 |
84 | export function useMounted(fn) {
85 | useEffect(fn, [])
86 | }
87 |
88 | export function useDestroyed(fn) {
89 | useEffect(() => fn, [])
90 | }
91 |
92 | export function useUpdated(fn, deps) {
93 | const isMount = useRef(true)
94 | useEffect(() => {
95 | if (isMount.current) {
96 | isMount.current = false
97 | } else {
98 | return fn()
99 | }
100 | }, deps)
101 | }
102 |
103 | export function useWatch(getter, cb, options) {
104 | ensureCurrentInstance()
105 | if (isMounting) {
106 | currentInstance.$watch(getter, cb, options)
107 | }
108 | }
109 |
110 | export function useComputed(getter) {
111 | ensureCurrentInstance()
112 | const id = ++callIndex
113 | const store = currentInstance._computedStore
114 | if (isMounting) {
115 | store[id] = getter()
116 | currentInstance.$watch(getter, val => {
117 | store[id] = val
118 | }, { sync: true })
119 | }
120 | return store[id]
121 | }
122 |
123 | export function withHooks(render) {
124 | return {
125 | data() {
126 | return {
127 | _state: {}
128 | }
129 | },
130 | created() {
131 | this._effectStore = {}
132 | this._refsStore = {}
133 | this._computedStore = {}
134 | },
135 | render(h) {
136 | callIndex = 0
137 | currentInstance = this
138 | isMounting = !this._vnode
139 | const ret = render(h, this.$attrs, this.$props)
140 | currentInstance = null
141 | return ret
142 | }
143 | }
144 | }
145 |
146 | export function hooks (Vue) {
147 | Vue.mixin({
148 | beforeCreate() {
149 | const { hooks, data } = this.$options
150 | if (hooks) {
151 | this._effectStore = {}
152 | this._refsStore = {}
153 | this._computedStore = {}
154 | this.$options.data = function () {
155 | const ret = data ? data.call(this) : {}
156 | ret._state = {}
157 | return ret
158 | }
159 | }
160 | },
161 | beforeMount() {
162 | const { hooks, render } = this.$options
163 | if (hooks && render) {
164 | this.$options.render = function(h) {
165 | callIndex = 0
166 | currentInstance = this
167 | isMounting = !this._vnode
168 | const hookProps = hooks(this.$props)
169 | Object.assign(this._self, hookProps)
170 | const ret = render.call(this, h)
171 | currentInstance = null
172 | return ret
173 | }
174 | }
175 | }
176 | })
177 | }
178 |
--------------------------------------------------------------------------------