With the release of Vue 3.2 a new composition tool was made available for us, called expose
.
Have you ever created a component that needs to make a few methods and properties available to the template, but wish that those methods were private to the component and not being able to be called by the parent?
If you are building an open source component or a library, chances are you want to keep some of the internal methods private. Before Vue 3.2, this was not easy to accomplish since everything that was declared in the options API in methods
or data
for example was made publicly available so that the template could access it.
The same is true of the composition API. Everything that we return out of the setup
method can be accessed directly by the parent.
Composition API
Let’s look at a practical example. Imagine we have a component that creates a counter, and each second it updates that counter.
📃 MyCounter.vue
<template>
<p>Counter: {{ counter }}</p>
<button @click="reset">Reset</button>
<button @click="terminate">☠️</button>
</template>
<script>
import { ref } from 'vue'
export default {
setup () {
const counter = ref(0)
const interval = setInterval(() => {
counter.value++
}, 1000)
const reset = () => {
counter.value = 0
}
const terminate = () => {
clearInterval(interval)
}
return {
counter,
reset,
terminate
}
}
}
</script>
From a composition point of view, I would like for parent components to be able to call the reset
method directly if needed — but I want to keep the terminate
function and the counter
ref only available to the component.
If we instantiate this component in a parent, App.vue
for example, and we attach a ref
to it, we can easily allow the parent to call the reset
method because it has been exposed along with terminate
when we returned it from setup.
📃 App.vue
<template>
<MyCounter ref="counter" />
<button @click="reset">Reset from parent</button>
<button @click="terminate">Terminate from parent</button>
</template>
<script>
import MyCounter from '@/components/MyCounter.vue'
export default {
name: 'App',
components: {
MyCounter
},
methods: {
reset () {
this.$refs.counter.reset()
},
terminate () {
this.$refs.counter.terminate()
}
}
}
</script>
If we run this right now and click either the reset or terminate buttons on the parent, both will work.
Let’s be explicit about what we want to expose
to the parent so that only the reset
function is available.
📃 MyCounter.vue
<script>
import { ref } from 'vue'
export default {
setup (props, context) {
const counter = ref(null)
const interval = setInterval(() => {
counter.value++
}, 1000)
const reset = () => {
counter.value = 0
}
const terminate = () => {
console.log(interval)
clearInterval(interval)
}
context.expose({ reset })
return {
counter,
reset,
terminate
}
}
}
</script>
Notice that we added the props
and context
params to the setup function. We need to have the context available to us because this is where the expose
function lives. We could also use destructuring like so: { expose }
.
Next, we use context.expose
to declare an object of elements that we want to expose to the parent that instantiates this component; in this case we are only going to make the reset
function available.
If we run the example again, and click the “Terminate from parent” button, we will get a JavaScript error.
Uncaught TypeError: this.$refs.counter.terminate is not a function
The terminate function is no longer available and our private API is now inaccessible.
Options API
I have purposely chosen to do the first example using the composition API because of the second use case of the expose
function, however I want you to know that it is also possible to use this method in the options API.
In order to write the above component with the declared expose
, we could rewrite it as follows.
📃 MyCounter.vue
export default {
created () { ... },
data: () => ({ counter: null }),
methods: {
reset () { ... },
terminate () { ... }
},
expose: ['reset']
}
Notice that we have added a new options API property expose
that allows us to pass in an array, where the string 'reset'
is the name of the function that we are making publicly available.
Composition API Render functions
A very powerful and flexible way to create components is to leverage the power of render functions. This is not new to Vue 3, however with the creation of the composition API we now have the flexibility of returning the composition h
function directly from a setup method.
This poses a problem, because the whole return
statement in our setup
function is just the h
method with the nodes that the component is creating.
If at this point we choose to expose something to the parent, we have the inverse problem as the one we saw before. Nothing is being exposed because nothing is being returned except the DOM elements.
Let’s rewrite the MyCounter.vue
component to use this method.
📃 MyCounter.vue
<script>
// The template has been deleted
import { ref, h } from 'vue'
export default {
setup (props, context) {
const counter = ref(0)
const interval = setInterval(() => {
counter.value++
}, 1000)
const reset = () => {
counter.value = 0
}
const terminate = () => {
clearInterval(interval)
}
// context.expose({ reset })
return () => h('div', [
h('p', `Counter: ${counter.value}`),
h('button', { onClick: reset }, 'Reset'),
h('button', { onClick: terminate }, 'Terminate')
])
}
}
</script>
Notice that we have imported h
from Vue at the top, since we need to use it to create our DOM elements.
I have also commented out the context.expose
method for now to illustrate the problem.
The return statement now replicates the DOM structure we had before with the <template>
and if we run the example we are able to click through the Reset and Terminate buttons on the element correctly.
However, if we click on the “Reset from parent” button now, we run into an error.
Uncaught TypeError: this.$refs.counter.reset is not a function
The reset
method is no longer being exposed since it’s not being returned by the setup
function. To fix this we need to uncomment our context.expose
call and make it available once again.
Wrapping up
The new expose
method is very intuitive and easy to implement in our components. It clears up a couple of very important composition problems that would have merited even a complete component rewrite in the past, so even if its not your day-to-day, go-to API, it’s something worth keeping nearby in your developer tool belt.
The repository for this article can be found here: https://github.com/Code-Pop/vue3-expose