SSR Safe Dynamic IDs in Vue

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.

Custom Directives

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 applied
created(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 updated
beforeUpdate(el, binding, vnode, prevVnode) {},
// called after the parent component and
// all of its children have updated
updated(el, binding, vnode, prevVnode) {},
// called before the parent component is unmounted
beforeUnmount(el, binding, vnode, prevVnode) {},
// called when the parent component is unmounted
unmounted(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) => {
// ...
}

Adding in Dynamic IDs

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>

Making it Work with SSR

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() };
},
}

Using the Custom Directive in Nuxt 3

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.

Conclusion

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.