DEV Community

loading...
Cover image for Reactive Responsive Design: Part 1

Reactive Responsive Design: Part 1

ngconf profile image ng-conf ・5 min read

Michael Madsen | ng-conf | Sep 2020

Reactive Responsive Design is a term I coined to describe a new paradigm for front-end design. It’s rooted in responsive design and builds into it the principles of reactive programming with RxJS. What we get in return for this is a cleaner, more maintainable, more flexible, and more testable app.

At the heart of responsive design is the media query. The media query is a css3 standard that conditionally applies styling based on a specific css query. The most commonly used options are min-width and max-width, which when used together, provides a range of screen widths where styles are applied.

The problem is that media queries are a mess. Here are some reasons why:

  1. Where is my code!?
    This is something I’ve often observed. Let’s say you have a web page with a lot of styling, and it has 5 responsive breakpoints. You need to take a look at the styling for the title on the page. You open the css file and find 1000+ lines of css with the title code spread throughout.

  2. Teams can end up with different queries.
    Especially when using modern JS frameworks you run the risk of different parts of a page having different breakpoints. This is an issue because your app could end up in a weird state on some screen sizes. What if a phone with an extra wide screen became popular and most of your app adjusted to mobile view on the expected width but the menu bar was built with a different query and was showing a desktop version. While we can address this issue with standards I find it much more dependable to have things enforced by code. This enforcement can not be achieved with media queries.

  3. Can only query screen width.
    Media queries are very limited in their abilities. This severely restricts the layout options you have with them. When using the width query, all you can do is apply different styles depending on the full width of the screen. That means you have to resort to more complicated processes to change layouts when, for instance, a sidebar menu is expanded or collapsed.

  4. Code will always load.
    This is one of the most irritating things about media queries. The devices with the most constrained resources (phones) are also the devices that display the most abbreviated user interface. With media queries, all of the elements that are hidden still have to be generated. That means that the devices with the greatest overhead to generate a screen are the devices with the smallest screens (phones).

  5. Hard to test.
    I am a big fan of testing. The problem with media queries is that if we were to test them it would have to be from an E2E test where we actually build the app and validate that the elements are laying out the desired way. Yuck.

What is Reactive Responsive Design

Reactive responsive design is the idea that we can observe screen size changes using an RxJS Observable. This will allow us to group classes in the css together without query bloat, codify breakpoints, break on things other than screen width, conditionally load components, and test.

How it Works
The first question is, how do we know when the screen is in a size range? What I do is use the window.matchMedia function. This is a native javaScript function that takes a string argument holding a media query. I then watch for changes in the status of the query (matched/not matched) and store those results in RxJS Subjects.

Here is what my class looks like:

import { BehaviorSubject, Observable } from 'rxjs';

export class RxRs {
  private topWindow: Window;
  private windowSizeSubjects: SizeSubjects = {};

  constructor() {
    this.topWindow = this.getWindow();
  }

  observe(query: string): Observable<boolean> {
    const mql = this.topWindow.matchMedia(query);
    let subject = this.windowSizeSubjects[mql.media];

    if (!subject) {
      this.windowSizeSubjects[mql.media] = new BehaviorSubject(mql.matches);
      mql.addListener(this.testQuery.bind(this));
      subject = this.windowSizeSubjects[mql.media];
    }

    return subject.asObservable();
  }

  private testQuery(e: any, subjects = this.windowSizeSubjects): void {
    const subject = subjects[e.media];
    if (subject) {
      subject.next(e.matches);
    }
  }

  private getWindow(): Window {
    return window.top;
  }
}

interface SizeSubjects {

}
Enter fullscreen mode Exit fullscreen mode

Lets break it down

First we get the top window reference. We will be interacting with the window in order to know the screen size.

 constructor() {
    this.topWindow = this.getWindow();
  }
Enter fullscreen mode Exit fullscreen mode

Next we have the core of the Reactive Responsive paradigm.

observe(query: string): Observable<boolean> {
    const mql = this.topWindow.matchMedia(query);
    let subject = this.windowSizeSubjects[mql.media];

    if (!subject) {
      this.windowSizeSubjects[mql.media] = new BehaviorSubject(mql.matches);
      mql.addListener(this.testQuery.bind(this));
      subject = this.windowSizeSubjects[mql.media];
    }

    return subject.asObservable();
  }
Enter fullscreen mode Exit fullscreen mode

The observe function is what is called by your app (ideally a service so you can standardize your break-points).

First, observe passes the query argument to the window matchMedia function. That will give us a MediaQueryList object which we will use to check our cache. If we are already tracking that query we will just return the existing Observable. Otherwise, we create a BehaviorSubject, set its initial value, and call the addListener function on the MediaQueryList which triggers whenever the matching state on the query changes.

The result is a class we can call, pass a media query to, and reference an Observable that emits when the query state changes! It even caches the queries, so if you request the same query again you will get the same observable back.

The observe function accepts any valid media query as an input; not just width and height. Do you want an observable returning you the orientation of the screen? What about the use of a pointing device (like a mouse)? Or, how fast the user’s device can update the screen? These options and more are available in the media query spec allowing you to do very complex layouts in a unified way all without bloating your css into an unreadable blob.

I’ve made a npm library called rxrs so that you don’t need to write your own implementation of this class. The next article will illustrate using rxrs to standardize on breakpoints and address the other issues with media queries discussed in this article.
To see how to apply rxrs and address the the issues with media queries check out part 2 here!

ng-conf: The Musical is coming

ng-conf: The Musical is a two-day conference from the ng-conf folks coming on April 22nd & 23rd, 2021. Check it out at ng-conf.org

Discussion (0)

Forem Open with the Forem app