-import { withHooks, useState, h, nextTick, useEffect } from '../src'
+import { withHooks, useState, h, nextTick, useEffect, Component } from '../src'
import { renderIntsance, serialize, triggerEvent } from '@vue/runtime-test'
describe('hooks', () => {
expect(effect).toBe(1)
})
+ it('should be usable inside class', async () => {
+ class Counter extends Component {
+ render() {
+ const [count, setCount] = useState(0)
+ return h(
+ 'div',
+ {
+ onClick: () => {
+ setCount(count + 1)
+ }
+ },
+ count
+ )
+ }
+ }
+
+ const counter = renderIntsance(Counter)
+ expect(serialize(counter.$el)).toBe(`<div>0</div>`)
+
+ triggerEvent(counter.$el, 'click')
+ await nextTick()
+ expect(serialize(counter.$el)).toBe(`<div>1</div>`)
+ })
+
+ it('should be usable via hooks() method', async () => {
+ class Counter extends Component {
+ hooks() {
+ const [count, setCount] = useState(0)
+ return {
+ count,
+ setCount
+ }
+ }
+ render() {
+ const { count, setCount } = this as any
+ return h(
+ 'div',
+ {
+ onClick: () => {
+ setCount(count + 1)
+ }
+ },
+ count
+ )
+ }
+ }
+
+ const counter = renderIntsance(Counter)
+ expect(serialize(counter.$el)).toBe(`<div>0</div>`)
+
+ triggerEvent(counter.$el, 'click')
+ await nextTick()
+ expect(serialize(counter.$el)).toBe(`<div>1</div>`)
+ })
+
it('useEffect with empty keys', async () => {
// TODO
})
export interface APIMethods<P = {}, D = {}> {
data(): Partial<D>
+ hooks(): any
render(props: Readonly<P>, slots: Slots, attrs: Data, parentVNode: VNode): any
}
_queueJob: ((fn: () => void) => void) | null = null
_isVue: boolean = true
_inactiveRoot: boolean = false
+ _hookProps: any = null
constructor(props?: object) {
if (props === void 0) {
export const reservedMethods: ReservedKeys = {
data: 1,
render: 1,
+ hooks: 1,
beforeCreate: 1,
created: 1,
beforeMount: 1,
import { ComponentInstance } from './component'
import { isFunction, isReservedKey } from '@vue/shared'
+import { warn } from './warning'
+import { isRendering } from './componentUtils'
const bindCache = new WeakMap()
const renderProxyHandlers = {
get(target: ComponentInstance<any, any>, key: string, receiver: any) {
+ let i: any
if (key === '_self') {
return target
- } else if (
- target._rawData !== null &&
- target._rawData.hasOwnProperty(key)
- ) {
+ } else if ((i = target._rawData) !== null && i.hasOwnProperty(key)) {
// data
+ // make sure to return from $data to register dependency
return target.$data[key]
- } else if (
- target.$options.props != null &&
- target.$options.props.hasOwnProperty(key)
- ) {
+ } else if ((i = target.$options.props) != null && i.hasOwnProperty(key)) {
// props are only proxied if declared
+ // make sure to return from $props to register dependency
return target.$props[key]
} else if (
- target._computedGetters !== null &&
- target._computedGetters.hasOwnProperty(key)
+ (i = target._computedGetters) !== null &&
+ i.hasOwnProperty(key)
) {
// computed
- return target._computedGetters[key]()
+ return i[key]()
+ } else if ((i = target._hookProps) !== null && i.hasOwnProperty(key)) {
+ // hooks injections
+ return i[key]
} else if (key[0] !== '_') {
- if (__DEV__ && !(key in target)) {
- // TODO warn non-present property
+ if (__DEV__ && isRendering && !(key in target)) {
+ warn(
+ `property "${key}" was accessed during render but does not exist on instance.`
+ )
}
const value = Reflect.get(target, key, receiver)
if (key !== 'constructor' && isFunction(value)) {
value: any,
receiver: any
): boolean {
+ let i: any
if (__DEV__) {
if (isReservedKey(key) && key in target) {
- // TODO warn setting immutable properties
+ warn(`failed setting property "${key}": reserved fields are immutable.`)
return false
}
- if (
- target.$options.props != null &&
- target.$options.props.hasOwnProperty(key)
- ) {
- // TODO warn props are immutable
+ if ((i = target.$options.props) != null && i.hasOwnProperty(key)) {
+ warn(`failed setting property "${key}": props are immutable.`)
return false
}
}
- if (target._rawData !== null && target._rawData.hasOwnProperty(key)) {
+ if ((i = target._rawData) !== null && i.hasOwnProperty(key)) {
target.$data[key] = value
return true
} else {
import { createRenderProxy } from './componentProxy'
import { handleError, ErrorTypes } from './errorHandling'
import { warn } from './warning'
+import { setCurrentInstance, unsetCurrentInstance } from './experimental/hooks'
let currentVNode: VNode | null = null
let currentContextVNode: VNode | null = null
initializeProps(instance, props, (currentVNode as VNode).data)
}
+export let isRendering = false
+
export function renderInstanceRoot(instance: ComponentInstance): VNode {
let vnode
try {
+ setCurrentInstance(instance)
+ if (instance.hooks) {
+ instance._hookProps =
+ instance.hooks.call(instance.$proxy, instance.$props) || null
+ }
+ if (__DEV__) {
+ isRendering = true
+ }
vnode = instance.render.call(
instance.$proxy,
instance.$props,
instance.$attrs,
instance.$parentVNode
)
+ if (__DEV__) {
+ isRendering = false
+ }
+ unsetCurrentInstance()
} catch (err) {
handleError(err, instance, ErrorTypes.RENDER)
}
let isMounting: boolean = false
let callIndex: number = 0
-const hooksState = new WeakMap<ComponentInstance, HookState>()
+const hooksStateMap = new WeakMap<ComponentInstance, HookState>()
export function setCurrentInstance(instance: ComponentInstance) {
currentInstance = instance
currentInstance = null
}
+function getHookStateForInstance(instance: ComponentInstance): HookState {
+ let hookState = hooksStateMap.get(instance)
+ if (!hookState) {
+ hookState = {
+ state: observable({}),
+ effects: []
+ }
+ hooksStateMap.set(instance, hookState)
+ }
+ return hookState
+}
+
export function useState<T>(initial: T): [T, (newValue: T) => void] {
if (!currentInstance) {
throw new Error(
)
}
const id = ++callIndex
- const { state } = hooksState.get(currentInstance) as HookState
+ const { state } = getHookStateForInstance(currentInstance)
const set = (newValue: any) => {
state[id] = newValue
}
}
}
effect.current = rawEffect
- ;(hooksState.get(currentInstance) as HookState).effects[id] = {
+ getHookStateForInstance(currentInstance).effects[id] = {
effect,
cleanup,
deps
injectEffect(currentInstance, 'unmounted', cleanup)
injectEffect(currentInstance, 'updated', effect)
} else {
- const record = (hooksState.get(currentInstance) as HookState).effects[id]
+ const record = getHookStateForInstance(currentInstance).effects[id]
const { effect, cleanup, deps: prevDeps = [] } = record
record.deps = deps
if (!deps || deps.some((d, i) => d !== prevDeps[i])) {
export function withHooks(render: FunctionalComponent): new () => Component {
return class ComponentWithHooks extends Component {
static displayName = render.name
- created() {
- hooksState.set((this as any)._self, {
- state: observable({}),
- effects: []
- })
- }
render(props: Data, slots: Slots, attrs: Data, parentVNode: VNode) {
setCurrentInstance((this as any)._self)
const ret = render(props, slots, attrs, parentVNode)