├── .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 | --------------------------------------------------------------------------------