- 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 - aPhase
.
- 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.
thearnica / react-media-match
React made responsible - media queries backed by state machinery
react-media-match
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 andMedia Pickers
to pick a value depending on the current media. - π£ Provide
hooks
interface forpickers
- π§ 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 someContext
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)