DEV Community

Discussion on: Media Queries in JS

Collapse
 
darrylnoakes profile image
Darryl Noakes • Edited

I currently use it for a bunch of things, with automatically detecting user theme preference being my most common and handy use.

With Vue, it's as simple as making the callback modify a reactive variable.
You can also easily make a "composable", which is very similar to your React example.

Based partly on the VueUse library's one:

useMediaQuery.ts

import { onBeforeUnmount } from "vue";

export useMediaQuery(query: string) {
  // Create the media query
  const mediaQuery = window.matchMedia(query);
  // Create a ref to hold the state of the match,
  // initialized with the current value of `matches`.
  // This is what we will return.
  const matches = ref(mediaQuery.matches);

  // Create a handler to update the value of the ref.
  const handler = (event: MediaQueryListEvent) => {
    matches.value = event.matches;
  }

  // Attach the handler to the media query.
  if ("addEventListener" in mediaQuery)
    mediaQuery.addEventListener("change", handler);
  else
    mediaQuery.addListener(handler);

  // Attach a callback that removes the event listener when the component is unmounted.
  onBeforeUnmount(() => {
    if ("removeEventListener" in mediaQuery)
      mediaQuery.removeEventListener("change", handler);
    else
      mediaQuery.removeListener(handler);
  });

  return matches;
}
Enter fullscreen mode Exit fullscreen mode

AwesomeComponent.vue

...

<script setup>
import { watch, computed } from "vue";

import { useMediaQuery } from "@/composables/useMediaQuery";

// We can use this ref like any other,
// to do cool things like watching it or making computed refs.
const { matches } = useMediaQuery("(prefers-color-scheme: dark)");

// Watch the ref and change the theme based on the user's preference.
watch(matches, (value) => {
  // enable/disable dark mode based on `value`.
});

// We can use this `color` ref in the template, and it will automatically update.
const color = computed(() => {
  return matches ? "dark" : "primary";
});
</script>

...
Enter fullscreen mode Exit fullscreen mode

You do not really need to allow a callback to be passed in, as you can simply watch the returned ref.
Maybe the timing of reactivity updates would make it necessary, so that the callback gets called immediately when the media query changes, but I think that would be an extreme edge case.

useEventListener is also a common composable.

useEventListener.ts

import { isRef, unref, watch, onMounted, onBeforeUnmount } from "vue";

export function useEventListener(
  // the target could be reactive ref which adds flexibility
  target: Ref<EventTarget | null> | EventTarget,
  event: string,
  handler: (e: Event) => any
) {
  // If it is a ref, use a watcher that removes the event listener
  // from the previous target and attaches it to the new target.
  let stopWatcher = () => {};
  if (isRef(target)) {
    stopWatcher = watch(target,
      (value, oldValue) => {
        oldValue?.removeEventListener(event, handler);
        value?.addEventListener(event, handler);
      },
      { immediate: true }, // Run the callback immediately.
    );
  } else {
    // Else use the mounted hook
    onMounted(() => {
      target.addEventListener(event, handler);
    });
  }

  // Create a function that cleans up the event listener.
  const stop = () => {
    stopWatcher();
    unref(target)?.removeEventListener(event, handler);
  };

  // Clean it up when the component is being unmounted.
  onBeforeUnmount(stop);

  // Return the function so that the user can remove the event listener if they want to.
  return stop;
}
Enter fullscreen mode Exit fullscreen mode

Using this, we can rewrite useMediaQuery.

useMediaQuery.ts

import { useEventListener } from "@/composables/useEventListener";

export useMediaQuery(query: string) {
  // Create the media query
  const mediaQuery = window.matchMedia(query);
  // Create a ref to hold the state of the match,
  // initialized with the current value of `matches`.
  // This is what we will return.
  const matches = ref(mediaQuery.matches);

  // Create a handler to update the value of the ref.
  const handler = (event: MediaQueryListEvent) => {
    matches.value = event.matches;
  }

  // Use the composable to handle the event listener.
  useEventListener(mediaQuery, "change", handler);

  return matches;
}
Enter fullscreen mode Exit fullscreen mode

Note: this will no longer work with browsers that only support addListener and removeListener and not addEventListener and removeEventListener.

We could also make this optionally take a ref to a string instead of just a string, and update the media query when it is changed, like what happens with useEventListener. Or we could simplify useEventListener :).
But for useEventListener, having a reactive target makes sense, as this is useful in many cases, while useMediaQuery doesn't really need that functionality.