- Start Date: 2020-02-27
- Target Major Version: 3.x
- Reference Issues: (fill in existing related issues, if any)
- Implementation PR: (leave this empty)
Introduce a new expose
option to declare component's public API available for parent components via refs
. Restrict access to component's context by default.
At the moment you can access the whole component's context without any limitations when using refs
.
<template>
<input ref="input">
</template>
<script>
export default {
name: 'MyInput',
methods: {
focus() { this.$refs.input.focus() }
}
}
</script>
<template>
<MyInput ref="input" />
</template>
<script>
export default {
name: 'Parent',
mounted() {
this.$refs.input.focus() // you can access anything on the context
}
}
</script>
After the change you'll be required to expose your data and methods explicitly.
<template>
<input ref="input">
</template>
<script>
export default {
name: 'MyInput',
expose: ['focus'],
methods: {
focus() { this.$refs.input.focus() },
blur() { this.$refs.input.blur() },
}
}
</script>
<template>
<MyInput ref="input" />
</template>
<script>
export default {
name: 'Parent',
mounted() {
this.$refs.input.focus() // exposed
this.$refs.input.blur() // error, not exposed
}
}
</script>
Right now component's context is free to access by anyone and that brings up a number of issues:
- You can not guarantee consistent component's behaviour due to external modifications. You can modify components data and won't be able to tell where that change came from.
- There's no contract between receiver and provider components. This could lead to refactoring problems when there's an unused method within the component, but it's required by another component and there's no easy way to tell that.
- No clear separation of data that is required by the component itself and other components.
To fix these issues components would be required to explicitly declare their public interface.
Components should not be able to directly access other components context anymore. To declare component's public interface authors must use a new expose
options and describe the data that should be exposed.
A several expose
configurations should be supported.
<script>
export default {
expose: ['foo', 'bar', 'baz'],
data() {
return {
foo: null
}
},
methods: {
bar() {},
},
computed: {
baz() {}
}
}
</script>
<script>
export const BAR_SYMBOL = Symbol()
export default {
expose: {
foo: 'bar', // expose 'foo' as 'bar'
bar: BAR_SYMBOL // expose 'bar' as a BAR_SYMBOL
},
data() {
return {
foo: null
}
},
methods: {
bar() {}
}
}
</script>
Functional configuration is more complicated because you can easily loose reactivity there.
<script>
export default {
expose() {
const { foo, bar } = this
return {
foo, // not reactive
bar
}
},
data() {
return {
foo: null
}
},
methods: {
bar() {},
}
}
</script>
To solve this issue you could either go with the Composition API or wrap your data inside an object.
<script>
export default {
expose() {
const { foo, bar } = this
return {
foo, // foo's value is reactive
bar
}
},
data() {
return {
foo: {
value: null
}
}
},
methods: {
bar() {},
}
}
</script>
Composition API example
<script>
import { ref } from 'vue'
export default {
name: 'MyComponent',
setup() {
const foo = ref(null)
const bar = () => {}
return {
foo,
bar
}
},
expose() {
const { foo, bar } = this
return {
foo, // reactive
bar
}
},
}
</script>
The exposed interface above could be utilized as following:
<template>
<MyComponent :ref="myComponent" />
</template>
<script>
export default {
mounted() {
console.log(this.$refs.myComponent.foo.value)
this.$refs.myComponent.bar()
}
}
</script>
To expose refs use mounted
hook and object as a wrapper to preserve reactivity.
<template>
<input ref="input">
</template>
<script>
export const INPUT_EXPOSED = Symbol()
export default {
expose() {
const { exposed } = this
return {
[INPUT_EXPOSED]: exposed
}
},
data() {
return {
exposed: {
inputRef: null
}
}
},
mounted() {
this.exposed.inputRef = this.$refs.input
}
}
</script>
Or function refs:
<template>
<input :ref="(input) => exposed.inputRef = input">
</template>
<script>
export const INPUT_EXPOSED = Symbol()
export default {
expose() {
const { exposed } = this
return {
[INPUT_EXPOSED]: exposed
}
},
data() {
return {
exposed: {
inputRef: null
}
}
},
}
</script>
Alternative way of accessing refs:
<template>
<input ref="input">
</template>
<script>
export const GET_INPUT_REF = Symbol()
export default {
name: 'MyInput',
expose() {
return {
[GET_INPUT_REF]: () => {
return this.$refs.input
}
}
},
}
</script>
<template>
<MyInput ref="input">
</template>
<script>
import { GET_INPUT_REF } from 'MyInput.vue'
export default {
mounted() {
console.log(this.$refs.input[GET_INPUT_REF]())
}
}
</script>
expose
should be executed after data init,refs
then would require a lot of fiddling around to preserve reactivity (wrapping exposed values with an object at a minimum)- Could be difficult to implement
- Not backwards compatible (but could only warn in compatibility build for example)
- Increased bundle size (could be disabled in production)
- Decreased performance: a mechanism like a proxy is necessary to prevent specific properties to be accessible (could be disabled in production)
Using events to expose your component's public interface:
<template>
<input ref="input">
</template>
<script>
export default {
mounted() {
const exposed = {
focusInput: () => {
this.$refs.input.focus()
}
};
this.$emit('ready', exposed);
}
}
</script>
Pros:
- Does not require any API change
- Easy to implement
- Fixes
refs
issue
Cons:
- Can not be enforced by the framework (unless context is always unavailable in refs)
- Forces to be extra careful with context
- Creates an unnecessary data flow
Components containing any data required by the parent component via refs
would be required to explicitly declare their public interface.
A loose
mode configuration in createApp
could be provided to opt-out of this behaviour.
-
Should it work with mixins?
Pros:
- Clear contract between mixins and component
Cons:
- Would require a lot of changes if you use mixins extensively
- Rises entry barrier for mixin usage
- Does not solve other issues with mixins (implicit props, extending context)
- Could be complicated to implement (mixin can extend context but should only access exposed parts of it)
- Unclear whether mixins should be able to extend the
expose
property
-
Should it work with
$parent
access? -
Should it work in tests?
Pros:
- Explicit testing interface
- Tests for
expose
interface specifically
Cons:
- Could lead to exposing too much data just for testing which defeats the purpose