Replacing an event bus with composition API/built-in functions in Vuejs3

Hello,

I am still learning Vuejs 3 (and JS in general) so for some of the simple tasks that might be easy to solve with the right tools - I come up with my own solutions (that are more complex sometimes :smiley: )

This time I think I have done something that is worth sharing with other people. Basically I think I have created some kind of pattern for when you need to notify a certain part of the app about an event.

Approach 1, using mitt:

Composable function

import mitt from "mitt";
const emitter = mitt();

export default function useEmitter() {
	return {
	  emitter 
	};
}

In component A

<script>
import useEmitter from "@/use/useEmitter.js";

export default {
  setup() {
	  const useEmitter = useEmitter();

	  useEmitter.emitter.emit("clicked");
	}
}
</script>

In component B

<script>
import useEmitter from "@/use/useEmitter.js";

export default {
  setup() {
	  const useEmitter = useEmitter();

	  useEmitter.emitter.on("clicked", () => {
		console.log("Clicked!");
	  });
	// p.s. don't forget to unsubscribe in onBeforeUnmount
	}
}
</script>

Approach 2, using ref/watch

Composable function

import { ref } from "vue";

const clickedToggle = ref(false);

export default function useEmitter() {
  const notifyClicked = () => {
	clickedToggle.value = !clickedToggle.value;
  };

  return {
	clickedToggle,
	notifyClicked,
  };
}

In component A

<script>
import useEmitter from "@/use/useEmitter.js";

export default {
  setup() {
	  const useEmitter = useEmitter();

	  useEmitter.notifyClicked();
	}
}
</script>

In component B

<script>
import { watch } from "vue";
import useEmitter from "@/use/useEmitter.js";

export default {
  setup() {
	  const useEmitter = useEmitter();

	  watch(useEmitter.clickedToggle, () => {
		console.log("Clicked!");
	  });
	}
}
</script>

Pros and cons associated with both solutions

mitt

pros

  • supports passing event arguments (emit('name', arguments))

cons

  • need to unsubscribe from events explicitly
  • dependency on 3rd party tool and all related cons (bundle size, maintenance, updates, learning curve)

using in-built functions

pros

  • no need to learn anything new
  • no need to install new dependencies (and all associated perks)
  • no need to unsubscribe from events

cons

  • does not support passing arguments to events (see edit)

Epilogue

I guess I have not discovered something new. People must have used this for ages. It would be nice to hear from VueJS veterans (or just in general from Javascript devs) if this solution could even be considered a good programming practice or not… Or should I just stick to using an event bus for simple cases like this (event with no arguments).

p.s. also I have not tested the code above. I have just typed it in notepad so there might be some typos making the code not to compile, but the general idea should be there.

Edit (19.01.2022)

I now understand that you could even support “event arguments” by reading the value of the reference object being watched! i.e. in composable function:

  const notifyClicked = (newValue) => {
	clickedToggle.value = newValue;
  };

and then when you need to read the argument/s you do:

watch(useEmitter.clickedToggle, () => {
	console.log("Clicked!");
	console.log(useEmitter.clickedToggle.value); // do something here
});

With this - the con is gone :slight_smile:

2 Likes

Hey me again :wink:

In regards to last edit - I have to make a small correction. If you want the trigger to work multiple times with the same value - you have to make this value unique. watch function only triggers when value that is changed is a… new value. My hack involves separating trigger (computed/ref value) and arguments like so:

import { ref } from "vue";

const routeCacheDestroyTrigger = ref(false);
let routeCacheDestroyTriggerArguments = undefined;

export default function useCachedState() {
  const notifyCacheDestroyRequired = (value) => {
	routeCacheDestroyTriggerArguments = value;
	routeCacheDestroyTrigger.value = !routeCacheDestroyTrigger.value;
  };

  const getRouteCacheDestroyTriggerArguments = () => {
	return routeCacheDestroyTriggerArguments ;
  };

  return {
	routeCacheDestroyTrigger,
	notifyCacheDestroyRequired,
	getRouteCacheDestroyTriggerArguments,
  };
}

So now we have 1 function that is a setter, 1 is a getter, and the trigger itself for watching only! (do not use get/set directly on trigger!).

Use as:

watch(useCachedStateInstance.routeCacheDestroyTrigger, () => {
  destroyCache(useCachedStateInstance.getRouteCacheDestroyTriggerArguments());
})

The only con that I can think of to this approach (it’s an important one!) is, if you want to spam the events like crazy then there is a chance that the argument value that you are going to read will be in fact the new value and not the old value that you expect! So there is definitely some edge cases that you either have to avoid or stick to using other solutions.

Since there were no replies to this topic - I think I should close it? Anyways feel free to comment even if /when the topic is closed!

Now I am wondering if the following would solve the problem I mentioned in previous reply? :slight_smile:

import { ref } from "vue";

const routeCacheDestroyTrigger = ref({
  args: undefined,
});

export default function useCachedState() {
  const notifyCacheDestroyRequired = (value) => {
    routeCacheDestroyTrigger.value = {
      //ts: new Date().getTime(),
      args: value,
    };
  };

  return {
    routeCacheDestroyTrigger,
    notifyCacheDestroyRequired,
  };
}

use as

watch(useCachedStateInstance.routeCacheDestroyTrigger, (value) => {
  destroyCache(value.args);
});

Since I don’t have such use-cases when I need to spam more than one event a-second I can’t tell if it solves all of the problems… But it looks like a more solid version than previous proposed solution.