In many cases, we need to generate unique IDs for elements dynamically.
But we want this to be stable through SSR so we don’t get any hydration errors.
And while we’re at it, why don’t we make it a directive so we can easily add it to any element we want?
I’ll explain how this works and how I got here, but this is the directive:
const generateID = () => Math.floor(Math.random() * 1000);const directive = {getSSRProps() {return { id: generateID() };},}
When using it with Nuxt, we need to create a plugin so we can register the custom directive:
const generateID = () => Math.floor(Math.random() * 1000);export default defineNuxtPlugin((nuxtApp) => {nuxtApp.vueApp.directive("id", {getSSRProps() {return { id: generateID() };},});});
In Nuxt 3.10+, you can also use the useId
composable instead:
<template><div :id="id" /></template><script setup>const id = useId();</script>
You can see a working demo of it here.
To create a custom directive in Vue, we use an object syntax and specify any number of hooks that we need for our directive (these hooks are pulled from the docs):
const myDirective = {// called before bound element's attributes// or event listeners are appliedcreated(el, binding, vnode, prevVnode) {},// called right before the element is inserted into the DOM.beforeMount(el, binding, vnode, prevVnode) {},// called when the bound element's parent component// and all its children are mounted.mounted(el, binding, vnode, prevVnode) {},// called before the parent component is updatedbeforeUpdate(el, binding, vnode, prevVnode) {},// called after the parent component and// all of its children have updatedupdated(el, binding, vnode, prevVnode) {},// called before the parent component is unmountedbeforeUnmount(el, binding, vnode, prevVnode) {},// called when the parent component is unmountedunmounted(el, binding, vnode, prevVnode) {}}
In many cases, all we need are the mounted
and updated
hooks. Since these are often running the same logic, there’s a function shorthand we can use that gets called once for the mounted
hook and then once for every updated
hook:
const directive = (el, binding, vnode, prevVnode) => {// ...}
In order to generate dynamic IDs, we’ll create ourselves a simple function to generate a random number:
const generateID = () => Math.floor(Math.random() * 1000);
For the directive, we only need the mounted
hook since we want the ID to be static:
const directive = {mounted(el) {el.id = generateID();},};
To add the directive to our Vue app, we need to use the directive
method on the app
object, usually found in our app.vue
file:
const app = createApp({})app.directive('id', {mounted(el) {el.id = generateID();},});
Now, we can add it to any element the same as any directive. Remember that we need to add the v-*
prefix:
<template><div v-id>This ID is dynamically generated</div></template>
Normally, custom directives are ignored by Vue during SSR because they typically are there to manipulate the DOM. Since SSR only renders the initial DOM state, there’s no need to run them, so they’re skipped.
But there are some cases where we actually need the directives to be run on the server, such as with our dynamic ID directive.
That’s where getSSRProps
comes in. It’s a special function on our directives that is only called during SSR, and the object returned from it is applied directly to the element, with each property becoming a new attribute of the element:
getSSRProps(binding, vnode) {// ...return {attribute,anotherAttribute,};}
Updating our directive to use getSSRProps
gives us this:
const generateID = () => Math.floor(Math.random() * 1000);const directive = {getSSRProps() {return { id: generateID() };},}
To use this in a Nuxt 3 app, we need to define a Nuxt plugin using defineNuxtPlugin
. We’ll add this to a new file at plugins/id.ts
:
const generateID = () => Math.floor(Math.random() * 1000);export default defineNuxtPlugin((nuxtApp) => {nuxtApp.vueApp.directive("id", {getSSRProps() {return { id: generateID() };},});});
We’re able to grab the current instance of the Nuxt app, and then from there we can grab the Vue app and then add the directive like we normally would.
Making an SSR-safe directive is pretty straightforward with the use of getSSRProps
, and I’m sure there are a ton of cool use cases for this — dynamically generating IDs is just scratching the surface.
You can go to the docs to learn more about how custom directives in Vue work.