DEV Community

Cover image for Take the Responsivebility
Anton Korzunov
Anton Korzunov

Posted on • Updated on

Take the Responsivebility

  • Mobile. Tablet. Desktop.
  • Portrait. Landscape.
  • NightMode on. And Off.
  • Adaptive. Responsive.
  • Responsible.

It's Saturday. Beautiful shiny and a bit windy morning. And I have to write this article, as long as I had the power to do it, and I have responsibility.

Let's dive deeper into react media queries and different ways to use them as well as base application logic to match the current state of a device, where you applicating has been launched. That's not so easy. You cannot just useMediaQuery - query is just a question, not the asnwer.

@media

I am not sure are you familiar with CSS @media Rule, so I would briefly explain what it is.

Media queries are useful when you want to modify your site or app depending on a device's general type (such as print vs. screen) or specific characteristics and parameters (such as screen resolution or browser viewport width).

Media queries might depend on: screen size, pixel density, screen type, pointer device used(mouse/touch), user settings(like reduced colors or motions), and device aspect ratio. And this is mainly CSS stuff.

Colocation

In terms of CSS there are two common approaches to handle different media targets.

  • separation
  • colocation

Per-Media Separation

With separation, you shall first define all styles for your main target, and then media-per-media override these values. Sometimes, when you don't want to override values (donโ€™t undo, just do principle) - you may even skip the first part.

With mobile-first approaches, it's easy to make a layout work well on narrow screens, and then "undo" most of
it on desktop. But that's tricky because you have to keep track of what has been done outside of media queries,
and reset those values inside the desktop media query. You also end up writing a lot of CSS just to reset
values, and you can end up leaving CSS like margin-bottom: 0 you are not sure what.
The margin-bottom set for the .side element should only appear on mobile.
Instead of applying a margin by default on all screens, and removing it on desktop, we only apply it on mobile. Jeremy Thomas, CSS in 44 minutes

Rules are colocated on per media basis.

.box {
  position: absolute;
  left: 0;
  top: 0;
}

.bar {
  position: sticky;
  top: 0;
}

.button {
  composes: flex-center;
}

@media screen and (min-width: $tablet) {
  .box {
    top: 1rem;
  }

  .bar {
    justify-content: center;
    width: $pin-size;
  }
}

@media screen and (min-width: $desktop) {
  .box {
    top: 0rem;
  }

  .button {
   composes: flex-right;
  }
}
  • ๐Ÿ‘ - easy to understand how one screen differs from another
  • ๐Ÿ‘Ž - harder to change styles, easy to forgot update media override.

Per-Rule Separation

In per-rule separation, everything related to one rule should be defined in one rule.

Rules are colocated on per selector basis.

.box {
  position: absolute;
  left: 0;
  top: 0;

  @media screen and (min-width: $tablet) {
    & {
      top: 1rem;
    }
  }

  @media screen and (min-width: $desktop) {
    & {
      top: 0rem;
    }
  }
}

.bar {
  position: sticky;
  top: 0;

  @media screen and (min-width: $tablet) {
    & {
      justify-content: center;
      width: $pin-size;
    }
  }
}

.button {
  composes: flex-center;

  @media screen and (min-width: $desktop) {
    .button {
      composes: flex-right;
    }
  }
}

File with this pattern applied might look larger, however, after CSSO(or any other CSS optimiser) it would be reformatted to a per-media format.

  • ๐Ÿ‘ - easy to change styles, move styles and share styles.
  • ๐Ÿ‘Ž - harder to understand how one screen different from another

The choice is quite personal here, but with a component approach (in your mind) the second way is better. Especially from a maintenance point of view.

Colocation, coherence, distance, cognitive load and pattern matching are very big things for CSS world. Wanna know more?

Anyway - it's not a big deal to handle this from CSS point of view, and please always do it, if it's possible. SSR would say thank you, as well as browser. But that's about React?

HTML/React

And there is a big problem with React, mostly from The Great Divide - React developers are not (and not required) fluent with CSS. So they invented their own ways.

Let's go thought most popular ways:

1๐Ÿ‘‘ React-responsive

React-responsive is actually very old repo, and was changing a bit over time. Today it has hooks and Components API

const isDesktopOrLaptop = useMediaQuery({
  query: '(min-device-width: 1224px)'
})
const isBigScreen = useMediaQuery({ query: '(min-device-width: 1824px)' })

<MediaQuery minDeviceWidth={1224} device={{ deviceWidth: 1600 }}>
   <p>You are a desktop or laptop</p>
   <MediaQuery minDeviceWidth={1824}>
     <p>You also have a huge screen</p>
   </MediaQuery>
</MediaQuery>

Each component, or hook, is a single operation, and does not have an else case.

2๐Ÿ‘‘ React-Media

React-Media from ReactTraining - takes the second place.

Component API is the main API

<Media query="(max-width: 599px)">
   {matches =>
     matches 
       ? <p>The document is less than 600px wide.</p>
       : <p>The document is at least 600px wide.</p>
   }
</Media>

Again - each component is a single operation, but this time does have an else case.

3๐Ÿ‘‘ Third places

Third place would be separated between 3 libraries. They all a FAR less popular than winners, but still - quite popular.

3/1 react-responsive-mixin

And the first is very strange (yet popular) react-responsive-mixin. Which is even not React 15 compatible. It's really that old createClass mixin, but still popular, and still works

var Component = React.createClass({
  mixins: [ResponsiveMixin],
  getInitialState: function () {
    return { url: '/img/large.img' };
  },
  componentDidMount: function () {
    this.media({maxWidth: 600}, function () {
      this.setState({url: '/img/small.jpg'});
    }.bind(this));
  },
  render: function () {
    return <img src={this.state.url} />;
  }
});

3/2 react-sizes

Second or Thirds - react-sizes. A redux-like query "state" manager

const mapSizesToProps = ({ width }) => ({
  isMobile: width < 480,
})

export default withSizes(mapSizesToProps)(MyComponent);

3/3 use-media

And the last is use-media, which is basically no more than a hook

const Demo = () => {
  // Accepts an object of features to test
  const isWide = useMediaLayout({minWidth: 1000});
  // Or a regular media query string
  const reduceMotion = useMediaLayout('(prefers-reduced-motion: reduce)');

  return (
    <div>
      Screen is wide: {isWide ? '๐Ÿ˜ƒ' : '๐Ÿ˜ข'}
    </div>
  );
};

The competition is over, all fired ๐Ÿ”ฅ

All libraries above are doing something very very wrong. Matching queries is not what you need.

Again what is media query:

Media queries are useful when you want to modify your site or app depending on a device's

And what does mean modify, and how "grouping" in terms of CSS could help you here?

Probably, let's double check how the problem could be solved from HTML way. And by HTML way I've assumed Bootstrap! bootstrap's responsibility utilities.

  • The .hidden-*-up classes hide the element when the viewport is at the given breakpoint or wider. For example, .hidden-md-up hides an element on medium, large, and extra-large viewports.
  • The .hidden-*-down classes hide the element when the viewport is at the given breakpoint or smaller. For example, .hidden-md-down hides an element on extra-small, small, and medium viewports.

This works roughly the same for any "atomic" CSS framework - just define a few classes and call it a day

// tailwind
<!-- Width of 16 by default, 32 on medium screens, and 48 on large screens -->
<img class="w-16 md:w-32 lg:w-48" src="...">

// bootstrap
<div class="col-md-6 col-lg-4 col-xl-3">
   <h2>HTML</h2>
</div>

// bulma
<div class="column is-half-mobile  is-one-quarter-tablet></div>

What is crucial here - everything is defined in one place. One class defines how component should work in all variances of target devices.

Let's think about this.

Finite State Machines

Yes! We started from CSS and here Parallel Finite State Machines comes to play.

Finite State Machine could be in only one state at one point of time. As long as state for React users might mean something different( like state!) let's use another term - a Phase.

  • Could water be hot and cold. Yes, but not simultaneously.
  • Could display be wide and small? Yes, but not simultaneously.
  • Could orientation be different? Yes, but not simultaneously.

Device is represented by many different states, some simple boolean, but some are more complex, which coexist simultaneously.

You may use "single query" to handle "boolean" states, but could not use it for states like screen size, as long as it could be up 5(easy!) different targets. And that's mean that you have to make up to 5 decisions simultaneously. Ok, let's do just 3.

<MediaMatcher
    mobile={"render for mobile"}
    tablet={"render for tablet"}
    desktop={"render desktop"}
/>

const title = useMedia({
  mobile: "Hello",
  tablet: "Hello my friend",
  desktop: "Hello my dear friend, long time no see",
});

This is the same sort of colocation we have with the "Per-Rule Separation", and the same sort we might have with "Atomic CSS".

Probably, some additional CSS-world patterns should be applicable:

// would render "render for mobile" for the "missed" tablet
<MediaMatcher
    mobile={"render for mobile"}
    // tablet={"render for tablet"}
    desktop={"render desktop"}
/>

// desktop would inherit "Hello my friend" from tablet
const title = useMedia({
  mobile: "Hello",
  tablet: "Hello my friend",
  // desktop: "Hello my dear friend, long time no see",
});

The rule is simple - pick the value to the left, also known as a mobile-first. And there is no way you might "forget" to provide some value for a specific target, especially if that target did not exists yesterday (you know, designers love to change their mind).

Media matching should not be just conditions in JavaScript. It shall be decisions, forks, switches.

Existing "media matches" gives your ability to match one media query, and some of them let do something in the else branch.

As I mention above - size based media queries might require more than two decisions made in one place. However - there is a subset of media matching, where logic is still binary, regardless how many phases you have in real. It's Above, and Below. Or Display-only and Dont-display.

Some libraries, like smooth-ui has Up and Down breakpoints, and with combination they are letting you to match any intervals you might need.

I hope the same "intervals" are doable with the approach I am talking about.

// dont display on mobile
<MediaMatcher
    mobile={null}
    tablet={"something to render above mobile"}
/>

// dont display on mobile
<MediaMatcher
    mobile={"something to render only `till tablet`(on mobile)"}
    tablet={null}
/>

You also might create alternative states, for the extra stuff media queries might give you:

const HoverMedia = createMediaMatcher({
  mouseDevice: "(hover: hover)",
  touchDevice: "(hover: none)",
});
// here is a trick - the order matters.
// we are putting a server("false") branch last, 
// so it would be a nearest "value to the left"

const MyComponent = () => {
  const autoFocus = HoverMedia.useMedia({
    touchDevice: false,
    mouseDevice: true,
  });

  // do not autofocus inputs on mobile
  // to prevent Virtual Keyboard opening
  return <input autoFocus={autoFocus}/>
}

Even ClientSide and ServerSide stuff could be managed this way. There are components which shall not be ServerSide rendered, or could be rendered on the client in "other way".

const SideMedia = createMediaMatcher({
  client: false,
  server: true,
});

// you can flip Server/Client only after "hydration" pass
// or SSR-ed HTML could not match CSR-ed one.
const SideMediaProvider = ({children}) => {
  const [isClient, setClient] = useState(false);
  useEffect(() => setClient(true), []);

  return (
    <SideMedia.Mock client={isClient}>
     {children}
    </SideMedia.Mock>
  )
}

const DisplayOnlyOnClient = ({children}) => {
  const display = SideMedia.pickMatch({
    client: true,
    server: false,
  });

  return display ? children : null;
}

And there is one more thing - all examples above were from an existing library, which, although, is faaar from being popular.

GitHub logo thearnica / react-media-match

React made responsible - media queries backed by state machinery

react-media-match

Build Status coverage-badge NPM version bundle size downloads Greenkeeper badge

Media targets and "sensors" are not toys - they define the state of your Application. Like a Finite State Machine state Handle it holistically. Do not use react media query - use media match.

  • ๐Ÿ“ฆ all required matchers are built in
  • ๐Ÿ mobile-first "gap-less", and (!)bug-less approach.
  • ๐Ÿ’ป SSR friendly. Customize the target rendering mode and SSR for any device.
  • ๐Ÿ’ก Provides Media Matchers to render Components and Media Pickers to pick a value depending on the current media.
  • ๐ŸŽฃ Provide hooks interface for pickers
  • ๐Ÿง  Good typing out of the box - written in TypeScript
  • ๐Ÿš€ more performant than usual - there is only one top level query
  • ๐Ÿงจ Controllable matchers

Sandbox

https://codesandbox.io/s/react-media-match-example-g28y3

Usage

Use prebuild matchers or define your own

// custom
import { createMediaMatcher } from 'react-media-match';
const customMatcher = createMediaMatcher({
  portrait: '(orientation: portrait)',
  landscape: '(orientation: landscape)',
โ€ฆ

We created react-media-match to handle device state as application state, not to match random queries. We tackle responsibility problem quite responsible.

A note about performance

In short, there are two approaches to the API

  • most common is when you might specify media-query, or handle resize in any other way, in the place of use.
  • less common is using one central store, and all redux based media libraries like redux-mediaquery are doing it.

For example, that's how react-responsive and use-media are working:

useMediaQuery({ query: '(min-device-width: 1824px)' })

react-media is absolutely the same, even if it's component based. Your ability to provide any query to the component means that every components hold it's own subscription.

<Media query="(max-width: 599px)">

react-sizes? Every withSizes attach onResize listener to the window and act independently, even if all subscriptions are doing the same thing. Bonus - react-sizes also throttles the resize callback.
That's a mistake. To be more correct - all libraries are doing something wrong.

What's wrong with it?

It's a long story, but React works a bit differently for updates caused by React controlled code(event handlers, lifecycle methods, etc), and the non-controlled ones.

When you call setState inside life cycle event - it does nothing, but schedules update, which, among with other scheduled updates, would be executed after your code would return execution back to React.

However, you can't use React to add resize event listener to the window, as well as match query. Thus these callbacks would be executed in a React uncontrolled way, and in this case setState would be synchronous. Causing as much cascade tree updates, as much connected components you have.

There are only two ways to handle this:

  • use a single source of truth, like redux or some Context based state, sitting on top of your app.
  • use yet undocumented, but stable and adopted by many libraries ReactDOM.unstable_batchedUpdates to combine all updates in one..

Fun fact - many libraries implement callback throttling, but they are making the โ€‹performance even worse.


With great power comes great responsibility. You shall be more responsible for the responsive side of your application, and think about media queries as you think about state.

Take the Responsivebility.

PS: if you are not quite sure why do to need "responsive" on React side - here is another article.

Top comments (0)