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() {
//...
}
}
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);
};
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];
}
};
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)