DEV Community

Cover image for Media Queries in JS/TS done right
Cristian-Florin Calina for This is Learning

Posted on

Media Queries in JS/TS done right

Recently, I had a situation that involved listening to the screen width of an application.
There are a lot of methods of achieving this, but I wanted something simple, reusable & generic that did not impact performance too much.

I thought that listening to resize event on window and checking the innerWidth is way too slow especially since everything related to layout forces a reflow.

I discovered quickly while searching for solutions, the window.matchMedia() API and it seemed like that was the best solution (it is also supported in all of the major browsers).

The only thing left to do, was to implement a simple, reusable & generic solution that uses this underneath. Here's a helpful MDN link that got me started.

What I wanted was to be able to subscribe to different media queries from different components across the application, without creating new listeners.

The solution I came up with, was to create a static class (well, a class with static methods and variables), that would keep a list of queries & subscriptions for those queries (and a list of listener refs so it can be easily cleaned up).

export interface IQueryList {
    [key: string]: {
        query: MediaQueryList;
        subscribers: Array<(matchesQuery: boolean) => void>;
    };
}

interface IEventListener {
    [key: string]: (event: MediaQueryListEvent) => void;
}

export class MediaQueryListener {
    private static _subscribers: IQueryList = {};
    private static _eventListenersRefs: IEventListener = {};

    static subscribe() {
        //...
    }
    static unsubscribe() {
        //...
    }
}
Enter fullscreen mode Exit fullscreen mode

This class would expose 2 public methods: subscribe and unsubscribe.

The subscribe method takes a callback function that receives a boolean parameter representing if the query matches or not, and the actual query in a string format (eg: '(max-width: 768px)').
It then checks if there is already a subscription made for this query from some other place:

  • if it is, it only adds the callback to the list of subscribers for that query.
  • if it's not, it adds the query to the list, with the only subscriber being the passed callback, then it starts to listen to changes for the newly created query.

Eitherway, it also calls the callback instantly with the current query match.

static subscribe = (cb: (matchesQuery: boolean) => void, query: string) => {
    if (!this._subscribers[query]) {
        this._subscribers[query] = {
            query: window.matchMedia(query),
            subscribers: [ cb ],
        };

        // keep the refs so we can remove the listeners in unsubscribe
        this._eventListenersRefs[query] = event => {
            this._subscribers[query]
                .subscribers
                .forEach(subscriber => subscriber(event.matches));
        };

        // create the listener only on first subscription for the query
        this._subscribers[query]
            .query
            .addEventListener('change', this._eventListenersRefs[query]);
    } else {
        this._subscribers[query].subscribers.push(cb);
    }

    // Call the callback with the current value of the media query
    cb(window.matchMedia(query).matches);
};
Enter fullscreen mode Exit fullscreen mode

The unsubscribe method receives the same callback function & query in a string format, and removes the callback from the subscribers of that query. It also checks if there are no more subscribers left, and removes the media query listener so there are no hanging listeners left.

static unsubscribe = (cb: (matchesQuery: boolean) => void, query: string) => {
    if (!this._subscribers[query]) {
        return;
    }

    this._subscribers[query].subscribers = this._subscribers[query].subscribers.filter(subscriber => subscriber !== cb);

    if (this._subscribers[query].subscribers.length === 0) {
        this._subscribers[query].query.removeEventListener('change', this._eventListenersRefs[query]);

        delete this._subscribers[query];
        delete this._eventListenersRefs[query];
    }
};
Enter fullscreen mode Exit fullscreen mode

As I said, it's a basic wrapper on top of matchMedia api. There are probably multiple packages out there that achieve the same thing in a way or another, I did not research specifically for those packages in advance before creating my solution since I liked the idea of writing one.

Thanks for reading the article !

Top comments (0)