Understanding Vue 3's "expose"


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

Download the cheatsheets

Save time and energy with our cheat sheets.