recorded decision · public · no signup
Summary
What was chosen
- Components can now control what is publicly exposed on their component instance.adr ↗
- For the Options API, a new `expose` option is introduced, expecting an array of property keys to be exposed.adr ↗
- For the Composition API, the `setup` context provides an `expose` method expecting an object of actual values to be exposed.adr ↗
- The `defineExpose()` macro is provided for `<script setup>` and compiles into a runtime `expose()` call.adr ↗
- The `expose` option is ignored in mixins, and the `expose` function is context-bound and callable once to limit control.adr ↗
Constraints
- A component's public and internal interface is not always the same, requiring selective exposure.adr ↗
- Components using render functions from `setup()` or `<script setup>` currently cannot selectively expose properties.adr ↗
- Mixins and external composition functions should not be able to expose arbitrary properties on a component.adr ↗
Consequences
The recorded why
- Start Date: 2020-09-06
- Target Major Version: 3.x
- Reference Issues: #135, #210
Summary
Provide the ability for components to control what is publicly exposed on its component instance. This proposal unifies #135 and #210 with additional details.
Basic example
§ Options API
export default {
expose: ['increment'],
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
§ Composition API
import { ref } from 'vue'
export default {
setup(props, { expose }) {
const count = ref(0)
function increment() {
count.value++
}
// public
expose({
increment
})
// private
return { increment, count }
}
}
Here in the both cases, other components would only be able to access the increment method from this component, and nothing else.
Motivation
In Vue, we have a few ways to retrieve the "public instance" of a component:
- Via template refs
- Via the
$parentor$rootproperties
Up until now, the concept of the "public instance" is equivalent to the this context inside a component. However, this creates a number of issues:
-
A component's public and internal interface isn't always the same. A component may have internal properties that it doesn't want to expose to other components. In other cases a component may want to expose methods that are specifically meant to be used by other components.
-
A component returning render function from
setup()encloses all its state inside thesetup()closure, so nothing is exposed on the public instance, and there's currently no way to selectively expose properties while using this pattern. -
<script setup>is also compiled into (2) so has the same issue.
The proposed APIs solve these issues by giving components the ability to explicitly declare publicly exposed properties.
Detailed design
§ Options API
A new option, expose is introduced. It expects an array of property keys to be exposed:
// Child.vue
export default {
expose: ['increment'],
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
In a parent component:
<Child ref="child" />
this.$refs.child.increment() // ok
this.$refs.child.count // undefined
If expose is an empty array, then the component would be considered "closed" and no properties would be exposed.
Note:
exposeoption is only respected in the base component and ignored when used in mixins.
§ Composition API
In Composition API, the 2nd argument of setup (aka the "setup context") now also provides the expose method:
import { ref } from 'vue'
export default {
setup(props, { expose }) {
const count = ref(0)
function increment() {
count.value++
}
// public
expose({
increment
})
// private
return { increment, count }
}
}
The expose method expects an object of the actual values to be exposed.
In <script setup>
The defineExpose() macro compiles into runtime expose() call.
<script setup>
const count = ref(0)
function increment() {
count.value++
}
defineExpose({
increment
})
</script>
See relevant section in the <script setup> RFC.
Exposed Ref Unwrapping Behavior
Note the exposed instance unwraps refs similar to the normal public instance:
// in child
const count = ref(0)
expose({
count
})
// in parent
this.$refs.child.count // 0
§ Limiting Expose Control to Base Component
As raised in the discussions for a previous draft (#210), it would be very confusing if mixins and external composition functions are able to expose arbitrary properties. Therefore, the API is designed to restrict the capability to the base component only:
- The
exposeoption is ignored when encountered inmixinsorextendssources. - The Composition API
exposefunction is provided via the setup context instead of a global API (so that it cannot be imported in external composition functions) and can only be called once.
§ Additionally Exposed Properties
An experimental implementation has been shipped in prior versions and we found in some cases there are patterns or tools (e.g. vue-test-utils) that relies on the presence of Vue built-in instance properties like $el or $refs.
The public instance with explicit expose thus still exposes these built-in instance properties.
Drawbacks
N/A
Alternatives
N/A
Adoption strategy
This feature is additive and doesn't affect existing usage.
Unresolved Questions
§ Type Inference
Currently no Vue tooling has the ability to infer types of child component instances obtained from a ref.
A workaround is to explicitly export the public interface from the child component:
<!-- Child.vue -->
<script lang="ts">
export interface Api {
// ...
}
</script>
In parent:
<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'
import type { Api } from './Child.vue'
const childRef = ref<Api>()
</script>
<template>
<Child ref="childRef" />
</template>
The above does not affect the runtime API design and therefore should not block this RFC from being merged.
Source: active-rfcs/0042-expose-api.md ↗