@blocz/react-responsive
Version:
🔍 <Only /> displays some contents for a specific screen size
427 lines (321 loc) • 16.2 kB
Markdown
# /react-responsive <!-- omit in toc -->
`/react-responsive` is inspired by the `.visible` classes from [bootstrap 4](https://getbootstrap.com/docs/4.0/migration/#responsive-utilities) (and `.hidden` classes from [bootstrap 3](https://getbootstrap.com/docs/3.3/css/#responsive-utilities-classes)): it lets you show or hide components based on the current screen size.
[See changelog](https://github.com/bloczjs/react-responsive/blob/main/CHANGELOG.md)
## Table of contents <!-- omit in toc -->
1. [Installation](#installation)
2. [How to use](#how-to-use)
1. [`useMediaQuery()`](#usemediaquery)
2. [Media ranges](#media-ranges)
1. [Default media ranges](#default-media-ranges)
2. [Additional `Up` and `Down`](#additional-up-and-down)
3. [`useMediaRange()`](#usemediarange)
4. [`<Only>`](#only)
1. [`on` prop](#on-prop)
2. [`matchMedia`](#matchmedia)
3. [Render as component (deprecated)](#render-as-component-deprecated)
5. [Custom media ranges: `createMediaRanges()`](#custom-media-ranges-createmediaranges)
1. [Strictly typed](#strictly-typed)
2. [Stricter `<Only>`](#stricter-only)
3. [Units \& direction](#units--direction)
6. [`<MediaRangesProvider>` (deprecated)](#mediarangesprovider-deprecated)
1. [Add more media ranges](#add-more-media-ranges)
2. [Change default media ranges](#change-default-media-ranges)
3. [Units](#units)
4. [Direction](#direction)
3. [Comparison to other libraries](#comparison-to-other-libraries)
4. [`matchMedia` polyfill](#matchmedia-polyfill)
1. [Browser](#browser)
2. [Node](#node)
5. [React 16 / 17 support](#react-16--17-support)
6. [Deprecated APIs](#deprecated-apis)
7. [FAQ](#faq)
## Installation
```sh
# pnpm
pnpm add /react-responsive
# yarn
yarn add /react-responsive
# npm
npm install /react-responsive
```
## How to use
### `useMediaQuery()`
`useMediaQuery()` is a [hook](https://react.dev/reference/react/hooks) that detects if the given media query matches the current viewport.
```javascript
import React from "react";
import { useMediaQuery } from "@blocz/react-responsive";
const App = () => {
const isLandscape = useMediaQuery("(orientation: landscape)");
return <p>{isLandscape ? "Landscape mode" : "Portrait mode"}</p>;
};
```
[Learn more about CSS media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries)
### Media ranges
`/react-responsive` is based on the classic bootstrap breakpoints: `xs`, `sm`, `md`, `lg` and `xl`.
Unlike Bootstrap – where `xs`, `sm`, etc. are single breakpoints – `/react-responsive` uses **media ranges**: each name describes the interval _between_ two breakpoints, making ranges explicit and non-overlapping.
See [Why media ranges instead of breakpoints?](https://github.com/bloczjs/react-responsive/blob/main/FAQ.md#why-media-ranges-instead-of-breakpoints) for more context.
#### Default media ranges
By default, the media ranges are:
| Media range | From | To |
| ----------- | -----: | -------: |
| `xs` | 0px | 575px |
| `sm` | 576px | 767px |
| `md` | 768px | 991px |
| `lg` | 992px | 1199px |
| `xl` | 1200px | Infinity |
This makes it fully explicit: a `lg` device is not `md` nor `xl`.
#### Additional `Up` and `Down`
Each media range also comes with `{mediaRange}Up` and `{mediaRange}Down` variants – covering everything above or below that breakpoint:
| Media range | From | To |
| ----------- | -----: | -------: |
| `xsUp` | 0px | Infinity |
| `smUp` | 576px | Infinity |
| `mdUp` | 768px | Infinity |
| `lgUp` | 992px | Infinity |
| `xlUp` | 1200px | Infinity |
| Media range | From | To |
| ----------- | ---: | -------: |
| `xsDown` | 0px | 575px |
| `smDown` | 0px | 767px |
| `mdDown` | 0px | 991px |
| `lgDown` | 0px | 1199px |
| `xlDown` | 0px | Infinity |
> **Note:** `xsDown` is equivalent to `xs`, `xlUp` is equivalent to `xl`, and `xlDown`/`xsUp` match all screen sizes – these exist only for convenience.
#### `useMediaRange()`
`useMediaRange()` is a [hook](https://react.dev/reference/react/hooks) that detects if the given media range matches the current viewport.
```javascript
import React from "react";
import { useMediaRange } from "@blocz/react-responsive";
const App = () => {
const matchXl = useMediaRange("xl");
const matchMdDown = useMediaRange("mdDown");
const matchMdOrLg = useMediaRange("md lg");
return (
<ul>
{matchXl && <li>Visible on every "large" device</li>}
{matchMdDown && <li>Visible on every device smaller than or equal to "medium"</li>}
{matchMdOrLg && <li>Visible on every "medium" or "large" device</li>}
</ul>
);
};
```
#### `<Only>`
`<Only>` is the component equivalent of `useMediaRange()` and `useMediaQuery()`: it renders its children only when the condition matches.
##### `on` prop
The `on` prop behaves like `useMediaRange()`: it accepts a media range name (or a space-separated list of names) and makes `<Only>` render its children when any of the named ranges match.
```javascript
import React from "react";
import { Only } from "@blocz/react-responsive";
const App = () => (
<React.Fragment>
<Only on="xs">Only visible for extra small devices (portrait phones)</Only>
<Only on="sm">Only visible for small devices (landscape phones)</Only>
<Only on="md">Only visible for medium devices (tablets)</Only>
<Only on="lg">Only visible for large devices (desktops)</Only>
<Only on="xl">Only visible for extra large devices (large desktops)</Only>
<Only on="sm xl">Only visible for small AND extra large devices</Only>
</React.Fragment>
);
```
##### `matchMedia`
The `matchMedia` prop behaves like `useMediaQuery()`: it accepts any regular query supported by [window.matchMedia](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia).
```javascript
import React from "react";
import { Only } from "@blocz/react-responsive";
const App = () => (
<Only matchMedia="(min-device-width: 500px) and (orientation: landscape)">
Visible on every device bigger than "500px" and in landscape mode
</Only>
);
```
> **Note:** If you use `on` AND `matchMedia` together, the component renders if **any** of the media ranges matches **OR** if the media query is fulfilled (not AND).
##### Render as component (deprecated)
> ⚠️ Using the `as` prop on `<Only>` is **deprecated** and will be removed in v6.0.0.
> This is not considered as type-safe
The `as` prop makes `<Only>` render as a different element (any DOM tag or React component). Any props except `on`, `matchMedia`, and `as` are forwarded to it:
```javascript
import React from "react";
import { Only } from "@blocz/react-responsive";
const App = () => (
<ul>
<Only as="li" on="xs">
Only visible for extra small devices
</Only>
<Only as="li" on="sm">
Only visible for small devices
</Only>
<Only as="li" on="md">
Only visible for medium devices
</Only>
</ul>
);
```
The `as` prop also accepts React components:
```javascript
import React from "react";
import { Only } from "@blocz/react-responsive";
const Custom = ({ title, children }) => (
<React.Fragment>
<h3>{title}</h3>
<p>{children}</p>
</React.Fragment>
);
const App = () => (
<React.Fragment>
<Only as={Custom} title="xs" on="xs">
Only visible for extra small devices
</Only>
<Only as={Custom} title="sm" on="sm">
Only visible for small devices
</Only>
<Only as={Custom} title="md" on="md">
Only visible for medium devices
</Only>
</React.Fragment>
);
```
#### Custom media ranges: `createMediaRanges()`
`createMediaRanges()` is the recommended way to customize the media ranges. It returns an object `{ useMediaRange, Only }` bound to the ranges you pass in, with end-to-end TypeScript types.
```javascript
import { createMediaRanges, DEFAULT_MEDIA_RANGES } from "@blocz/react-responsive";
const { useMediaRange, Only } = createMediaRanges({
...DEFAULT_MEDIA_RANGES,
pxRange: [263, 863, { unit: "px" }],
emRange: [20, 40, { unit: "em" }],
});
```
If you want to re-use the same defaults as the top-level `<Only>` & `useMediaRange()`, you'll need to import & use `DEFAULT_MEDIA_RANGES`.
##### Strictly typed
The returned `useMediaRange()` accepts only the names that match the ranges you declared (plus the auto-generated `Up` and `Down` aliases). The passed string can hold a single name or a space-separated list, every media range will be typechecked:
```typescript
useMediaRange("md"); // ✅
useMediaRange("pxRangeUp"); // ✅
useMediaRange("mdDown"); // ✅
useMediaRange("md pxRange"); // ✅
useMediaRange("invalid"); // ❌ TS error
useMediaRange("md invalid"); // ❌ TS error – "md" is fine, "invalid" is not
```
This is also true for the returned `<Only>`:
```tsx
<>
<Only
// ✅
on="md pxRange"
>
…
</Only>
<Only
// ❌ TS error
on="lg invalid"
>
…
</Only>
</>
```
##### Stricter `<Only>`
Unlike the top-level `<Only>`, the `<Only>` returned from `createMediaRanges()` does not support the `as` prop (and does not forward additional props to an inner element).
##### Units & direction
Each range entry accepts one of these shapes: `[min, max]`, or `[min, max, { unit?, direction? }]`:
```javascript
const { Only } = createMediaRanges({
pxRange: [263, 863, { unit: "px" }],
emRange: [20, 40, { unit: "em" }],
yRange: [200, 400, { direction: "height" }],
});
```
#### `<MediaRangesProvider>` (deprecated)
> ⚠️ `<MediaRangesProvider>` is **deprecated** and will be removed in v6.0.0. Use [`createMediaRanges()`](#custom-media-ranges-createmediaranges) instead.
`<MediaRangesProvider>` defines all media range values.
Use it to inject or modify the media ranges (only use one `<MediaRangesProvider>` per build).
##### Add more media ranges
```javascript
import React from "react";
import { Only, MediaRangesProvider } from "@blocz/react-responsive";
const App = () => (
<MediaRangesProvider additionalMediaRanges={{ customRange: [263, 863] }}>
<Only on="customRange">Visible on every device from "263px" to "863px"</Only>
<Only on="customRangeUp">Visible on every device bigger than "263px"</Only>
<Only on="customRangeDown">Visible on every device smaller than "863px"</Only>
</MediaRangesProvider>
);
```
##### Change default media ranges
```javascript
import React from "react";
import { Only, MediaRangesProvider } from "@blocz/react-responsive";
const App = () => (
<MediaRangesProvider mediaRanges={{ sm: [263, 863] }}>
<Only on="sm">Visible on every device from "263px" to "863px"</Only>
<Only on="smUp">Visible on every device bigger than "263px"</Only>
<Only on="smDown">Visible on every device smaller than "863px"</Only>
</MediaRangesProvider>
);
```
**Warning**: This **overrides completely** the default media ranges, in this example, the other media ranges `xs`, `md`, `lg` and `xl` **are no longer defined!**
##### Units
You can specify which unit is going to be used for the media range by specifying in the 3rd option a "unit" key.
Every CSS unit is supported. The default unit is `px`.
```javascript
import React from "react";
import { Only, MediaRangesProvider } from "@blocz/react-responsive";
const App = () => (
<MediaRangesProvider
additionalMediaRanges={{
pxRange: [263, 863, { unit: "px" }],
emRange: [20, 40, { unit: "em" }],
}}
>
<Only on="pxRange">Visible on every device from "263px" to "863px"</Only>
<Only on="emRange">Visible on every device from "20em" to "40em"</Only>
</MediaRangesProvider>
);
```
##### Direction
You can specify which direction is used for the media queries (height or width).
By default, "width" is the chosen direction.
```javascript
import React from "react";
import { Only, MediaRangesProvider } from "@blocz/react-responsive";
const App = () => (
<MediaRangesProvider
mediaRanges={{
xRange: [300, 500, { direction: "width" }],
yRange: [200, 400, { direction: "height" }],
}}
>
<Only on="xRange">Visible on every device from "300px" to "500px" wide</Only>
<Only on="yRange">Visible on every device from "200px" to "400px" tall</Only>
</MediaRangesProvider>
);
```
## Comparison to other libraries
| Lib | Media ranges | Custom media ranges | Media query | `matchMedia` listener\* | hooks | SSR support |
| ------------------------------------------------------------------------------------- | -----------: | ------------------: | ----------: | ----------------------: | ----: | ----------: |
| [/react-responsive](https://npmx.dev/package/@blocz/react-responsive) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [react-responsive](https://npmx.dev/package/react-responsive) | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| [react-breakpoints](https://npmx.dev/package/react-breakpoints) | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
| [react-responsive-breakpoints](https://npmx.dev/package/react-responsive-breakpoints) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
\*: `matchMedia` listener event means that the library is built around `matchMedia.addListener(callback)` and not `window.addEventListener('resize', callback)` (which is faster because the callback is only triggered when the media query's state changes and not at every resize).
## `matchMedia` polyfill
### Browser
If you want to use `matchMedia` in browsers that don't support it, I'd recommend [`matchmedia-polyfill`](https://github.com/paulirish/matchMedia.js/).
### Node
If you want to mock `matchMedia` on Node to execute tests for instance, you can use [`mock-match-media`](https://github.com/Ayc0/mock-match-media/).
And if you need an example with `Jest`, `-library/react`, `React` and `@blocz/react-responsive`, you can take a look at [these tests](https://github.com/bloczjs/react-responsive/blob/main/packages/tests/src/__tests__/ssr.ts).
## React 16 / 17 support
`/react-responsive` relies on `useSyncExternalStore`. This function was added in React 18.
If you are on React 16.8+ / React 17, you'll need to use [use-sync-external-store](https://npmx.dev/package/use-sync-external-store) to polyfill `useSyncExternalStore`.
## Deprecated APIs
The terminology used by this library used to be "breakpoint". It was renamed to "media range" because each entry actually describes the range between two breakpoints rather than a single breakpoint.
For backward compatibility, the previous exports are still available but marked as ``, and will be removed in the next major release:
| Deprecated | Replacement |
| ---------------------------- | --------------------------------------------------------------- |
| `useBreakpoint()` | `useMediaRange()` |
| `<BreakpointsProvider>` | `<MediaRangesProvider>` |
| `<BreakpointsContext>` | `<MediaRangesContext>` |
| `breakpoints` prop | `mediaRanges` prop |
| `additionalBreakpoints` prop | `additionalMediaRanges` prop |
| `<MediaRangesProvider>` | [`createMediaRanges()`](#custom-media-ranges-createmediaranges) |
| `<MediaRangesContext>` | [`createMediaRanges()`](#custom-media-ranges-createmediaranges) |
## FAQ
For other questions, please take a look at our [FAQ document](https://github.com/bloczjs/react-responsive/blob/main/FAQ.md).