How to use Mitt.js + Vue.js 3 + composition API + TypeScript

How to use Mitt.js + Vue.js 3 + composition API + TypeScript

Easy way to supercharge your Vue.js application.

When you build an application with Vue.js, there comes a time when you start feeling overwhelmed and tired of the event system Vue.js offers you. I mean, it's super powerful and useful, but you can enhance your app, reuse code, organize your code, and improve maintainability by combining it with a Global Event Bus.

Create a composable for your event bus

How to create a composable to serve as a Global Event Bus in your Vue.js application ? Handling events efficiently becomes essential as your Vue.js project evolves. With this composable, you'll establish a centralized event management system that simplifies event handling, enhances code organization, and boosts maintainability.

// project/useEventBus.ts
import mitt, { EventType, Handler } from 'mitt';

export interface GlobalEvents extends Record<EventType, unknown> {}

const emitter = mitt<GlobalEvents>();

export function useEventBus() {
    return emitter
}

Choose a name for your event

Don't use simple strings. For consistency and usefulness, wrap them inside a constant. Whatever the shape, just wrap it!

// project/ResourceEvent.ts

export const EVENT_Resource = 'resource:post';
// OR
export const EVENT_Resource = {
    Post: 'resource:post',
    Patch: 'resource:patch',
} as const;

declare module './useEventBus' {
    interface GlobalEvents {
        [EVENT_Resource.Post]: YourAwesomeType;
        [EVENT_Resource.Patch]: YourAwesomeType;
        //...
    }
}

In order to use that event, you have to import it wherever you need it, like so:

import { EVENT_Resource } from 'project/ResourceEvent';
import { useEventBus } from 'project/useEventBus';

const { on, emit, off } = useEventBus();

on(EVENT_Resource.Post, () => { ... })
emit(EVENT_Resource.Post, { ... })
off(EVENT_Resource.Post, () => { ... })

Create the composable you need for your business logic

// project/useResourceHandler
import { EVENT_Resource } from 'project/ResourceEvent';
import { useEventBus } from 'project/useEventBus';
import { ref } from 'vue';

export function useResourceHandler() {
    const whatever = ref<YourAwesomeType>();

    const { on, emit } = useEventBus();

    on(EVENT_Resource.Post, (payload) => {
        whatever.value = payload;
    });

    function triggerEvent(obj: YourAwesomeType) {
        emit(EVENT_Resource.Post, obj);
    }

    return {
        whatever,
    }
}

Now you have your business logic wrapped into a composable, so you can use it inside a component:

// project/MyAwesomeComponent.vue

<template>
 <button @click="triggerEvent({name: 'fake'})">Fake button</button>
</template>

<script setup lang="ts">
import { useResourceHandler } from 'project/useResourceHandler'

const { triggerEvent, whatever } = useResourceHandler();
</script>

This is only for example purposes, but you can do way more and organize your code in another way that fits your needs.

So now you have everything set up, but if you leave it like that, you are going to have multiple handlers registered in the event bus for this event as the user goes back and forth in your interface. Each time you use your composable, you are asking to register the handler. So let's see what we need to do next.

Don't forget to unsubscribe

In Vue.js, you have the context of a Vue.js app, and the composition API makes things mount and unmount (component or composable). As mentioned in this section of the documentation (https://vuejs.org/guide/reusability/composables#side-effects): WE NEED TO CLEAN UP SIDE EFFECTS. In our case, subscribing to an event is a “side effect” of our logic, and so we need to clean it up.

Change your composable as below:

// project/useResourceHandler
import { EVENT_Resource } from 'project/ResourceEvent';
import { useEventBus } from 'project/useEventBus';
import { ref } from 'vue';

export function useResourceHandler() {
    const whatever = ref<YourAwesomeType>();

    const { on, emit, off } = useEventBus();

    on(EVENT_Resource.Post, (payload) => {
        whatever.value = payload;
    });

    function triggerEvent(obj: YourAwesomeType) {
        emit(EVENT_Resource.Post, obj);
    }

    onBeforeUnmount(() => {
        off(EVENT_Resource.post);
    });

    return {
        whatever,
    }
}

Now you realize it's kind of boring and repetitive to unsubscribe every time you subscribe to an event somewhere in your app. So to improve that, check the next extra step.

Generalize everything and DO NOT REPEAT YOURSELF

As your app grows, you are going to have hundreds of events to manage and use, and therefore, hundreds of lines of code to subscribe and then unsubscribe. It's kind of a pain to do that everywhere. So let's refactor our code a bit.

// project/useEventBus.ts
import mitt, { EventType, Handler } from 'mitt';
import { onBeforeUnmount } from 'vue';

const emitter = mitt<>();

export interface GlobalEvents extends Record<EventType, unknown> {}

export function useEventBus() {
    function extendedOn<Key extends keyof GlobalEvents>(
        type: Key,
        handler: Handler<GlobalEvents[Key]>,
    ): void {
        emitter.on(type, handler);
        onBeforeUnmount(async () => {
            emitter.off(type, handler);
        });
    }

    return { ...emitter, extendedOn };
}

You can improve that function by passing an options parameter to that function in order to be able to deactivate this extra behaviour if needed.

So now, to use it, the good thing is that we don't need to touch our components. Just go to your composable and change the function used to subscribe to an event.

// project/useResourceHandler
import { EVENT_Resource } from 'project/ResourceEvent';
import { useEventBus } from 'project/useEventBus';
import { ref } from 'vue';

export function useResourceHandler() {
    const whatever = ref<YourAwesomeType>();

    const { extendedOn, emit, off } = useEventBus();

    extendedOn(EVENT_Resource.Post, (payload) => {
        whatever.value = payload;
    });

    function triggerEvent(obj: YourAwesomeType) {
        emit(EVENT_Resource.Post, obj);
    }

    /* REMOVE THAT
    onBeforeUnmount(() => {
        off(EVENT_Resource.post);
    });*/

    return {
        whatever,
    }
}

And voilà! You're all set up to build an app with an amazing global Event Bus that allows you to reuse logic everywhere!