diff --git a/.babelrc b/.babelrc
deleted file mode 100644
index 4f06b0c..0000000
--- a/.babelrc
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "presets": [
- "@babel/preset-env",
- "@babel/preset-react"
- ]
-}
diff --git a/.gitignore b/.gitignore
index 0e8f2f0..81115fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,7 @@ yarn.lock
.env.development.local
.env.test.local
.env.production.local
+.eslintcache
npm-debug.log*
yarn-debug.log*
diff --git a/LICENSE b/LICENSE
index 16687e1..7fb1757 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2019-2020 Envato
+Copyright (c) 2019-2021 Envato
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 4439618..a2fc2c6 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
-
+
@@ -15,23 +15,32 @@
`react-breakpoints` allows you to respond to changes in a DOM element's size. You can change the evaluated logic and rendered output of components based on observed size changes in DOM elements. For example, you can change a dropdown menu to a horizontal list menu based on its parent container's width without using CSS media queries.
-## 📦 What's in the box?
+# 📦 What's in the box?
+
> No polling. No event listening. No sentinel elements. **Just a [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)!**
This package provides you with:
-* a [``](/docs/api.md#provider) to instantiate the `ResizeObserver`;
-* an [``](/docs/api.md#observe) component to observe changes in a DOM element and respond to them.
+- a [``](/docs/api.md#provider) to instantiate the `ResizeObserver`;
+- an [``](/docs/api.md#observe) component to observe changes in a DOM element and respond to them.
For power users this package also provides:
-* a [`useBreakpoints()`](/docs/api.md#usebreakpoints) hook to change a component's behaviour based on the observed size information in the nearest parent ``;
-* a [`useResizeObserver()`](/docs/api.md#useresizeobserver) hook to connect a DOM element in your component to the instantiated `ResizeObserver` on ``;
-* a [`useResizeObserverEntry()`](/docs/api.md#useresizeobserverentry) hook to retrieve the `ResizeObserverEntry` put on the nearest ``. This is what `useBreakpoints()` uses under the hood.
+- a [`useBreakpoints()`](/docs/api.md#usebreakpoints) hook to change a component's behaviour based on the observed size information in the nearest parent ``;
+- a [`useResizeObserver()`](/docs/api.md#useresizeobserver) hook to connect a DOM element in your component to the instantiated `ResizeObserver` on ``;
+- a [`useResizeObserverEntry()`](/docs/api.md#useresizeobserverentry) hook to retrieve the `ResizeObserverEntry` put on the nearest ``. This is what `useBreakpoints()` uses under the hood.
+
+# 🐉 Be careful using this package when…
-# 🚧 Developer status
+- …all you want is the low-level API stuff. See [@envato/react-resize-observer-hook](https://github.com/envato/react-resize-observer-hook).
+- …you want _real_ CSS Element Queries. At the end of the day, this is still a JS solution.
+- …you care deeply about [Cumulative Layout Shift](https://web.dev/cls/) on public pages. **Keep reading though, this package may still be of value to you!**
-Several projects within Envato are currently using this package, giving me confidence that the API is clear and the code adds value. The package is still in an early stage, but exposure to "the wild" will help reveal more edge-cases and hopefully make the package more robust overall.
+# 🏅 This package is _really good_ at…
+
+- …following the latest [draft spec](https://drafts.csswg.org/resize-observer/), giving you access to cutting edge features like `devicePixelContentBoxSize` and [per-fragment](https://drafts.csswg.org/css-break-3/) observation.
+- …performantly observing many elements with a single `ResizeObserver` instance. None of that "a new `ResizeObserver` instance per observed element" bloat that [some](https://github.com/ZeeCoder/use-resize-observer/blob/314b29c33cfcd2c51b8854b775b0a2a5c325d94a/src/index.ts#L151-L157) alternative packages implement.
+- …building highly-responsive private dashboards 📊. One key thing this package (and every other `ResizeObserver` package out there) can contribute negatively to is [Cumulative Layout Shifting](https://web.dev/cls/). At Envato we've had great success using this package on pages that are only visible after signing in, like our Author Dashboard. We've had less success using it in places where search engines can go, on components with responsive styles that changed the layout vertically. One of our company values is "Tell It Like It Is", so we're letting you know to **be mindful of when and how you use `ResizeObserver` for responsive layouts.**
# ⚡️ Quick start
@@ -41,54 +50,55 @@ Follow these **minimum required steps** to get started with `react-breakpoints`.
npm install @envato/react-breakpoints
```
-## Set up the provider
+## Wrap your component tree with the provider
-```javascript
+```jsx
import { Provider as ResizeObserverProvider } from '@envato/react-breakpoints';
-const App = () => (
-
- ...
-
-)
+const App = () => ...;
```
⚠️ **Caution** — You may need to pass some props to `` to increase browser support. Please refer to the [API Docs](/docs/api.md#provider).
-## Observe an element
-
-Everything you render through `` has access to the size of the element that is given `{...observedElementProps}`. This is called the "Observe Scope".
+## Observe an element and use the results
-```javascript
+```jsx
import { Observe } from '@envato/react-breakpoints';
-const MyObservingComponent = () => (
- (
+const exampleBreakpoints = {
+ widths: {
+ 0: 'mobile',
+ 769: 'tablet',
+ 1025: 'desktop'
+ }
+ };
+
+export const ExampleComponent = () => (
+
+ {({ observedElementProps, widthMatch = 'ssr' }) => (
)}
- />
+
);
```
See the [API Docs](/docs/api.md) for reference guides and usage examples.
-# Observing vs. Consuming boxes
+# Observing vs. Consuming `ResizeObserverSize`
+
+There is an important distinction between the `boxSize` you observe and the `boxSize` you pass to your breakpoints. See [Observing vs. Consuming `ResizeObserverSize`](/docs/boxSizes.md) for more information.
+
+# Re-rendering
+
+Using [`useResizeObserver()`](/docs/api.md#useresizeobserver), [`useResizeObserverEntry()`](/docs/api.md#useresizeobserverentry) or [`useBreakpoints()`](/docs/api.md#usebreakpoints) in your components causes them to re-render **every time a resize is observed**.
-There is an important distinction between the `box` you observe and the `box` you consume for triggering breakpoints. See [Observing vs. Consuming Boxes](/docs/boxes.md) for more information.
+In some cases, you may want to optimise this. If you only want to re-render your components when the returned breakpoint values actually change, use `React.useMemo` or `React.memo`.
# Re-rendering
-Using [`useResizeObserver()`](/docs/api.md#useresizeobserver), [`useResizeObserverEntry()`](/docs/api.md#useresizeobserverentry) or [`useBreakpoints()`](/docs/api.md#usebreakpoints) in your components causes them to re-render **every time a resize is observed**.
+Using [`useResizeObserver()`](/docs/api.md#useresizeobserver), [`useResizeObserverEntry()`](/docs/api.md#useresizeobserverentry) or [`useBreakpoints()`](/docs/api.md#usebreakpoints) in your components causes them to re-render **every time a resize is observed**.
In some cases, you may want to optimise this. If you only want to re-render your components when breakpoint values actually change, use `React.useMemo` or `React.memo`.
@@ -98,7 +108,7 @@ See [Server-Side Rendering](/docs/server-side-rendering.md) for more information
# Maintainers
-* [Marc Dingena](https://github.com/mdingena) (owner)
+- [Marc Dingena](https://github.com/mdingena) (owner)
# Contributing
diff --git a/docs/api.md b/docs/api.md
index f5ddc1f..e15a8ef 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -2,25 +2,25 @@
Common usage:
-* [``](#provider)
-* [``](#observe)
+- [``](#provider)
+- [``](#observe)
Advanced usage:
-* [`useBreakpoints()`](#usebreakpoints)
-* [`useResizeObserver()`](#useresizeobserver)
-* [`useResizeObserverEntry()`](#useresizeobserverentry)
-* [``](#context)
+- [`useBreakpoints()`](#usebreakpoints)
+- [`useResizeObserver()`](#useresizeobserver)
+- [`useResizeObserverEntry()`](#useresizeobserverentry)
+- [``](#context)
---
# ``
-Your app must include a ``. It creates a React context with a `ResizeObserver` instance as its value. This allows components nested under `` to be observed for changes in size.
+Your app must include a ``. It creates a React context provider with a `ResizeObserver` instance as its value. This allows components nested under `` to be observed for size changes.
## Reference guide
-```javascript
+```jsx
`. It creates a React context with a `ResizeO
```
-ponyfill — optional ResizeObserver constructor
+ponyfill — optional typeof ResizeObserver
+
+⚠️ **Caution** — `Provider` instantiates a `window.ResizeObserver` by default. [`window.ResizeObserver` currently has fair browser support](https://caniuse.com/mdn-api_resizeobserver_resizeobserver). You may pass a `ResizeObserver` constructor to `Provider` to use instead of `window.ResizeObserver`. I recommend [ponyfilling](https://ponyfill.com) using [`@juggle/resize-observer`](https://github.com/juggle/resize-observer). You can also [monkey patch](https://en.wikipedia.org/wiki/Monkey_patch) `window.ResizeObserver` and use `Provider` without the `ponyfill` prop.
-⚠️ **Caution** — `Provider` instantiates a `window.ResizeObserver` by default. [`window.ResizeObserver` currently has weak browser support](https://caniuse.com/#feat=mdn-api_resizeobserver_resizeobserver). You may pass a `ResizeObserver` constructor to `Provider` to use instead of `window.ResizeObserver`. I recommend [ponyfilling](https://ponyfill.com) using [`@juggle/resize-observer`](https://github.com/juggle/resize-observer). You can also [monkey patch](https://en.wikipedia.org/wiki/Monkey_patch) `window.ResizeObserver` and use `Provider` without the `ponyfill` prop.
## Usage
-```javascript
+```jsx
import { Provider as ResizeObserverProvider } from '@envato/react-breakpoints';
-import { ResizeObserver } from '@juggle/resize-observer'; // Ponyfill
+import { ResizeObserver } from '@juggle/resize-observer'; // optional ponyfill
-const App = () => (
-
- ...
-
-);
+const App = () => ...;
```
---
# ``
-You can observe size changes of an element's `box` by rendering it through ``'s `render` prop. Your render function receives a `observedElementProps` argument that you spread onto the DOM element you wish to observe. It also receives `widthMatch` and `heightMatch` arguments which match the values you assigned via ``'s `breakpoints` prop.
+You can observe changes of an element's `boxSize` by rendering it through ``'s `children`. Your child function receives a `observedElementProps` argument that you spread onto the DOM element you wish to observe. If you passed the `breakpoints` prop, the child function also receives `widthMatch` and `heightMatch` arguments which match the values you assigned to this prop.
-⚠️ **Important** — There is an important distinction between the `box` you observe and the `box` you consume for triggering breakpoints. See [Observing vs. Consuming Boxes](boxes.md) for more information.
+⚠️ **Important** — There is an important distinction between the `boxSize` you observe and the `boxSize` you pass to your breakpoints. See [Observing vs. Consuming `ResizeObserverSize`](boxSizes.md) for more information.
## Reference guide
-```javascript
+```jsx
+ {/* pass a child function */}
+ {
({
/* object of props to spread onto the element you wish to observe */
observedElementProps,
@@ -97,7 +95,7 @@ You can observe size changes of an element's `box` by rendering it through `
- {/* component without useBreakpoints() can still be told about breakpoints */}
+ {/* component without useBreakpoints() can still be told about breakpoint values */}
)
}
-/>
+
```
-box — optional String
+box — optional ResizeObserverBoxOptions
Depending on your implementation of `ResizeObserver`, the [internal `ResizeObserverEntry`](#useresizeobserverentry) can contain size information about multiple "boxes" of the observed element.
This library supports observing the following `box` options (but your browser may not!):
-* [`'border-box'`](https://caniuse.com/#feat=mdn-api_resizeobserverentry_borderboxsize)
-* [`'content-box'`](https://caniuse.com/#feat=mdn-api_resizeobserverentry_contentboxsize)
-* [`'device-pixel-content-box'`](https://github.com/w3c/csswg-drafts/issues/3554)
+- [`'border-box'`](https://caniuse.com/#feat=mdn-api_resizeobserverentry_borderboxsize)
+- [`'content-box'`](https://caniuse.com/#feat=mdn-api_resizeobserverentry_contentboxsize)
+- [`'device-pixel-content-box'`](https://github.com/w3c/csswg-drafts/pull/4476)
If `box` is left `undefined` or set to any value other than those listed above, `` will default to using information from `ResizeObserverEntry.contentRect`.
-⚠️ **Important** — There is an important distinction between the `box` you observe and the `box` you consume for triggering breakpoints. See [Observing vs. Consuming Boxes](boxes.md) for more information.
+⚠️ **Important** — There is an important distinction between the `boxSize` you observe and the `boxSize` you pass to your breakpoints. See [Observing vs. Consuming `ResizeObserverSize`](boxSizes.md) for more information.
+
-breakpoints — optional Object
+breakpoints — optional object
This prop accepts an object with a shape identical to the `options` object of [`useBreakpoints()`](#usebreakpoints).
+
+
+
+
+children — (args: RenderOptions) => ReactNode
+
+```javascript
+interface RenderOptions {
+ observedElementProps: {
+ ref: React.RefCallback
+ };
+ widthMatch: any;
+ heightMatch: any;
+}
+```
+
+The children prop takes a function with three arguments.
+
+- `observedElementProps` — object
+ Using the `...` spread operator, you apply this object to the DOM element you want to observe.
+- `widthMatch` — any
+ If you passed an object with a `widths` property to the `breakpoints` prop, this argument contains the currently matching width breakpoint value.
+- `heightMatch` — any
+ If you passed an object with a `heights` property to the `breakpoints` prop, this argument contains the currently matching height breakpoint value.
+
+Your function should at least return some JSX with `{...observedElementProps}` applied to a DOM element.
+
-render — Function
+render — (args: RenderOptions) => ReactNode
+
+If you prefer a render prop over `children`, you may use `render`. Note that if both provided, `` will use `children`.
+
+```javascript
+interface RenderOptions {
+ observedElementProps: {
+ ref: React.RefCallback
+ };
+ widthMatch: any;
+ heightMatch: any;
+}
+```
A render prop that takes a function with three arguments.
-* `observedElementProps` — Object
+- `observedElementProps` — Object
Using the `...` spread operator, you apply this object to the DOM element you want to observe.
-* `widthMatch` — any
+- `widthMatch` — any
If you passed an object with a `widths` property to the `breakpoints` prop, this argument contains the currently matching width breakpoint value.
-* `heightMatch` — any
+- `heightMatch` — any
If you passed an object with a `heights` property to the `breakpoints` prop, this argument contains the currently matching height breakpoint value.
Your function should at least return some JSX with `{...observedElementProps}` applied to a DOM element.
+
## Usage
-```javascript
+```jsx
import { Observe } from '@envato/react-breakpoints';
const MyObservingComponent = () => (
@@ -167,28 +206,26 @@ const MyObservingComponent = () => (
2160: '4K'
}
}}
- render={({ observedElementProps, widthMatch, heightMatch }) => (
+ >
+ {({ observedElementProps, widthMatch, heightMatch }) => (
<>
{/* this element is given a class based on a child sidebar's width */}
-
{/* this sidebar is observed */}
{/* this component receives one of the `heights` strings defined above based on the sidebar's height */}
- {/* this component also uses `useBreakpoints()` to adapt to the sidebar's size, but from outside the sidebar */}
+ {/* this component also uses useBreakpoints() to adapt to the sidebar's size, but from outside the sidebar */}
>
)}
- />
+
);
```
@@ -196,75 +233,78 @@ const MyObservingComponent = () => (
# `useBreakpoints()`
-⚠️ **Advanced usage** — This hook is used internally in [``](#observe) to enable the use of the optional `breakpoints` prop.
+⚠️ **Advanced usage** — This hook is used internally in [``](#observe) to enable the use of its optional `breakpoints` prop.
Components inside an "[``](#observe) scope" have access to its observations. The observed element's sizes are available on a [context](#context) via the `useBreakpoints()` hook.
The hook takes an `options` object as its first argument, which must include a `widths` or `heights` key (or both) with an object as its value. That object must have a shape of numbers as keys, and a value of any type. The value you set here is what will eventually be returned by `useBreakpoints()`.
-Optionally, you can include a `box` property, which — depending on your implementation of `ResizeObserver` — can target different observable "boxes" of an element. By default, the legacy `contentRect` property will be used by `useBreakpoints()`.
+Optionally, you can include a `box` property, which — depending on your implementation of `ResizeObserver` — can target different observable "boxes" of an element. By default, the legacy `contentRect` property will be used by `useBreakpoints()`, but I recommend you use one of the spec's new `ResizeObserverSize` box sizes.
-The hook takes an optional `ResizeObserverEntry` as its second argument. **If you pass one, `useBreakpoints()` will not fetch it from the [context](#context), so use caution!**
+The hook takes an optional `ResizeObserverEntry` as its second argument. **You probably don't need this, but know that if you pass one, `useBreakpoints()` will not fetch the entry from the [context](#context), so use caution!**
## Reference guide
```javascript
/* returns an array of matched breakpoint values */
-const [
- /* first element is the matched value from `widths` */
- widthValue,
-
- /* second element is the matched value from `heights` */
- heightValue
-] = useBreakpoints({
- /* (optional) target a box size of the observed element to match widths and heights on */
- box: 'border-box',
-
- /* (optional) must be specified if `heights` is not specified */
- widths: {
- /* keys must be numbers and are treated like CSS's @media (min-width) */
- 0: 'small', /* value can be of any type */
- 769: 'medium',
- 1025: 'large'
- },
+const {
+ /* matched value from `options.widths` */
+ widthMatch,
- /* (optional) must be specified if `widths` is not specified */
- heights: {
- /* keys must be numbers and are treated like CSS's @media (min-height) */
- 0: 'SD', /* value can be of any type */
- 720: 'HD Ready',
- 1080: 'Full HD',
- 2160: '4K'
- },
+ /* matched value from `options.heights` */
+ heightMatch
+} = useBreakpoints(
+ {
+ /* (optional) the boxSize of the observed element to pass to the breakpoint matching logic */
+ box: 'border-box',
- /* (optional) the box size fragment index to match widths and heights on (default 0) */
- fragment: 0
-},
+ /* (optional) must be specified if `heights` is not specified */
+ widths: {
+ /* keys must be numbers and are treated like CSS's @media (min-width) */
+ 0: 'small' /* value can be of any type */,
+ 769: 'medium' /* keys are evaluated in order */,
+ 1025: 'large'
+ },
+
+ /* (optional) must be specified if `widths` is not specified */
+ heights: {
+ /* keys must be numbers and are treated like CSS's @media (min-height) */
+ 0: 'SD' /* value can be of any type */,
+ 720: 'HD Ready' /* keys are evaluated in order */,
+ 1080: 'Full HD',
+ 2160: '4K'
+ },
+
+ /* (optional) the boxSize fragment index to match widths and heights on (default 0) */
+ fragment: 0
+ },
-/* (optional) a ResizeObserverEntry to use instead of the one provided on context */
-injectResizeObserverEntry);
+ /* (optional) a ResizeObserverEntry to use instead of the one provided on context */
+ injectResizeObserverEntry
+);
```
-options.box — optional String
+options.box — optional ResizeObserverBoxOptions
Depending on your implementation of `ResizeObserver`, the [internal `ResizeObserverEntry`](#useresizeobserverentry) can contain size information about multiple "boxes" of the observed element.
This library supports the following `box` options (but your browser may not!):
-* [`'border-box'`](https://caniuse.com/#feat=mdn-api_resizeobserverentry_borderboxsize)
-* [`'content-box'`](https://caniuse.com/#feat=mdn-api_resizeobserverentry_contentboxsize)
-* [`'device-pixel-content-box'`](https://github.com/w3c/csswg-drafts/issues/3554)
+- [`'border-box'`](https://caniuse.com/#feat=mdn-api_resizeobserverentry_borderboxsize)
+- [`'content-box'`](https://caniuse.com/#feat=mdn-api_resizeobserverentry_contentboxsize)
+- [`'device-pixel-content-box'`](https://github.com/w3c/csswg-drafts/pull/4476)
If `box` is left `undefined` or set to any value other than those listed above, `useBreakpoints()` will default to using information from `ResizeObserverEntry.contentRect`.
-⚠️ **Important** — There is an important distinction between the `box` you observe and the `box` you consume for triggering breakpoints. See [Observing vs. Consuming Boxes](boxes.md) for more information.
+⚠️ **Important** — There is an important distinction between the `boxSize` you observe and the `boxSize` you pass to your breakpoints. See [Observing vs. Consuming `ResizeObserverSize`](boxSizes.md) for more information.
+
-options.widths — optional Object
+options.widths — optional object
-`widths` must be an object with numbers as its keys. The numbers represent the minimum width the `box` must have for that key's value to be returned. The value of the highest matching width will be returned.
+`widths` must be an object with numbers as its keys. The numbers represent the minimum width the observed `boxSize.inlineSize` must be for that key's value to be returned. The value of the highest matching width will be returned, as if using multiple CSS `@media (min-width)` queries.
For example, when a width of `960` is observed, using the following `widths` object would return `'medium'`:
@@ -276,7 +316,7 @@ widths: {
}
```
-⚠️ **Caution** — If you do not provide `0` as a key for `widths`, you risk receiving `undefined` as a return value. This is intended behaviour, but makes it difficult to distinguish between receiving `undefined` because of a [Server-Side Rendering](server-side-rendering.md) scenario, or because the observed width is less than the next matching width.
+⚠️ **Caution** — If you do not provide `0` as a key for `widths`, you risk receiving `undefined` as a return value. This is intended behaviour, but makes it difficult to distinguish between receiving `undefined` because of a [Server-Side Rendering](server-side-rendering.md) scenario, or because the observed width is less than the lowest defined width.
For example, when a width of `360` is observed, using the following `widths` object would return `undefined`:
@@ -287,40 +327,14 @@ widths: {
}
```
-Values can be of _any_ type, you are not restricted to return `string` values.
-
-```javascript
-// Numbers
-const [visibleCarouselItems] = useBreakpoints({
- widths: {
- 0: 1,
- 769: 3,
- 1025: 4
- }
-});
-
-// Booleans
-const [showDropdown] = useBreakpoints({
- widths: {
- 0: true,
- 961: false
- }
-});
+Values can be of _any_ type, you are not restricted to return `string` values, and value types can be mixed for different keys.
-// Components
-const [Component] = useBreakpoints({
- widths: {
- 0: HamburgerMenu,
- 1381: HorizontalMenu
- }
-});
-```
-options.heights — optional Object
+options.heights — optional object
-`heights` must be an object with numbers as its keys. The numbers represent the minimum height the `box` must have for that key's value to be returned. The value of the highest matching height will be returned.
+`heights` must be an object with numbers as its keys. The numbers represent the minimum height the observed `boxSize.blockSize` must be for that key's value to be returned. The value of the highest matching height will be returned, as if using multiple CSS `@media (min-height)` queries.
For example, when a height of `1440` is observed, using the following `heights` object would return `'Full HD'`:
@@ -333,32 +347,36 @@ heights: {
}
```
-⚠️ **Caution**: If you do not provide `0` as a key for `heights`, you risk receiving `undefined` as a return value. This is intended behaviour, but makes it difficult to distinguish between receiving `undefined` because of a [Server-Side Rendering](server-side-rendering.md) scenario, or because the observed height is less than the next matching height.
+⚠️ **Caution**: If you do not provide `0` as a key for `heights`, you risk receiving `undefined` as a return value. This is intended behaviour, but makes it difficult to distinguish between receiving `undefined` because of a [Server-Side Rendering](server-side-rendering.md) scenario, or because the observed height is less than the lowest defined height.
+
+Values can be of _any_ type, you are not restricted to return `string` values, and value types can be mixed for different keys.
-Values can be of _any_ type, you are not restricted to return `string` values. See example in `widths` section above.
-options.fragment — optional Number
+options.fragment — optional number
The box sizes are exposed as sequences in order to support elements that have multiple fragments, which occur in multi-column scenarios. You can specify which fragment's size information to use for matching `widths` and `heights` by setting this prop. Defaults to the first fragment.
See the [W3C Editor's Draft](https://drafts.csswg.org/resize-observer-1/#resize-observer-entry-interface) for more information about fragments.
+
injectResizeObserverEntry — optional ResizeObserverEntry
-Allows you to force `useBreakpoints()` to use the `ResizeObserverEntry` you pass here in its calculations rather than retrieving the entry that's on the closest [``](#context).
+Allows you to force `useBreakpoints()` to use the `ResizeObserverEntry` you pass here in its calculations rather than retrieving the entry that's on the closest [``](#context). Because of the Rules of Hooks, `React.useContext()` will still be called but its returned value is ignored.
+
## Usage
-```javascript
+```jsx
import { useBreakpoints } from '@envato/react-breakpoints';
const MyResponsiveComponent = () => {
const options = {
+ box: 'border-box',
widths: {
0: 'mobile',
769: 'tablet',
@@ -366,13 +384,9 @@ const MyResponsiveComponent = () => {
}
};
- const [label] = useBreakpoints(options);
+ const { widthMatch: label } = useBreakpoints(options);
- return (
-
- This element is currently within the {label} range.
-
- );
+ return
This element is currently within the {label} range.
;
};
```
@@ -381,8 +395,9 @@ const MyResponsiveComponent = () => {
# `useResizeObserver()`
⚠️ **Advanced usage** — This hook is used internally in [``](#observe) to:
-* start and stop observing an element by passing a `ref` to a DOM element;
-* bind a standardised callback to all observations, and set the `observedEntry` on a [``](#context).
+
+- start and stop observing an element by passing a `ref` to a DOM element;
+- bind a standardised callback to all observations, and set the `observedEntry` on a [``](#context).
This hook takes an optional `options` object argument, which currently only supports a `box` option.
@@ -390,10 +405,10 @@ This hook takes an optional `options` object argument, which currently only supp
```javascript
const [
- /* ref to set on the element you want to observe */
+ /* ref callback to set on the element you want to observe */
ref,
- /* all of the observed element's box sizes */
+ /* all of the observed element's boxSizes */
observedEntry
] = useResizeObserver(
/* (optional) options object */
@@ -405,22 +420,23 @@ const [
```
-options.box — optional String
+options.box — optional ResizeObserverBoxOptions
Depending on your implementation of `ResizeObserver`, you may observe one of multiple "boxes" of an element to trigger an update of `observedEntry`. By default this option is not set, and the size information of the observed element comes from the legacy `contentRect` property.
This library supports the following `box` options (but your browser may not!):
-* [`'border-box'`](https://caniuse.com/#feat=mdn-api_resizeobserverentry_borderboxsize)
-* [`'content-box'`](https://caniuse.com/#feat=mdn-api_resizeobserverentry_contentboxsize)
-* [`'device-pixel-content-box'`](https://github.com/w3c/csswg-drafts/issues/3554)
+- [`'border-box'`](https://caniuse.com/#feat=mdn-api_resizeobserverentry_borderboxsize)
+- [`'content-box'`](https://caniuse.com/#feat=mdn-api_resizeobserverentry_contentboxsize)
+- [`'device-pixel-content-box'`](https://github.com/w3c/csswg-drafts/pull/4476)
+
+⚠️ **Important** — There is an important distinction between the `boxSize` you observe and the `boxSize` you pass to your breakpoints. See [Observing vs. Consuming `ResizeObserverSize`](boxSizes.md) for more information.
-⚠️ **Important** — There is an important distinction between the `box` you observe and the `box` you consume for triggering breakpoints. See [Observing vs. Consuming Boxes](boxes.md) for more information.
## Usage
-```javascript
+```jsx
import { useEffect } from 'react';
import { useResizeObserver, Context } from '@envato/react-breakpoints';
@@ -447,10 +463,10 @@ const MyObservedComponent = () => {
This is an observed element
);
-}
+};
```
-⚠️ **Hint** — This is for when you really need advanced behaviour. This is completely abstracted away for your convenience by [``](#observe).
+⚠️ **Hint** — This is for when you really need advanced behaviour, such as calculating logic based on the absolute width and height values rather than a few breakpoint values. The latter is completely abstracted away for your convenience by [``](#observe).
---
@@ -467,52 +483,44 @@ const resizeObserverEntry = useResizeObserverEntry(
/* (optional) a ResizeObserverEntry to use instead of the one provided on context */
injectResizeObserverEntry
);
-const fragmentIndex = 0;
/* retrieve width and height from legacy `contentRect` property */
-const {
- contentRectWidth: width,
- contentRectHeight: height
-} = resizeObserverEntry.contentRect;
+const { contentRectWidth: width, contentRectHeight: height } = resizeObserverEntry.contentRect;
/* retrieve width and height from `borderBoxSize` property of first fragment */
-const {
- borderBoxSizeWidth: inlineSize,
- borderBoxSizeHeight: blockSize
-} = resizeObserverEntry.borderBoxSize[fragmentIndex];
+const { borderBoxSizeWidth: inlineSize, borderBoxSizeHeight: blockSize } = resizeObserverEntry.borderBoxSize[0];
/* retrieve width and height from `contentBoxSize` property of first fragment */
-const {
- contentBoxSizeWidth: inlineSize,
- contentBoxSizeHeight: blockSize
-} = resizeObserverEntry.contentBoxSize[fragmentIndex];
+const { contentBoxSizeWidth: inlineSize, contentBoxSizeHeight: blockSize } = resizeObserverEntry.contentBoxSize[0];
/* retrieve width and height from `devicePixelContentBoxSize` property of first fragment */
const {
- devicePixelContentBoxSizeWidth: inlineSize,
- devicePixelContentBoxSizeHeight: blockSize
-} = resizeObserverEntry.devicePixelContentBoxSize[fragmentIndex];
+ inlineSize: devicePixelContentBoxSizeWidth,
+ blockSize: devicePixelContentBoxSizeHeight
+} = resizeObserverEntry.devicePixelContentBoxSize[0];
```
## Usage
-```javascript
+```jsx
import { useResizeObserverEntry } from '@envato/react-breakpoints';
const MyResponsiveComponent = () => {
const resizeObserverEntry = useResizeObserverEntry();
/**
- * Falsey if element from Context has not been observed yet.
+ * `null` if element from Context has not been observed yet.
* This is mostly the case when doing Server-Side Rendering.
*/
- if (!resizeObserverEntry) { /* ... */ };
+ if (!resizeObserverEntry) {
+ /* ... */
+ }
const { inlineSize: width, blockSize: height } = resizeObserverEntry.borderBoxSize[0];
let className;
- if (width >= 1025 ) {
+ if (width >= 1025) {
className = 'large';
} else if (width >= 769) {
className = 'medium';
@@ -521,14 +529,16 @@ const MyResponsiveComponent = () => {
}
return (
-
+ <>
{/* this element's className changes based on its observed border-box width */}
-
- )
-}
+ {/* CAUTION - beware of creating a circular dependency by changing the observed sizes within your classnames! */}
+
I'm being observed!
+ >
+ );
+};
```
-⚠️ **Hint** — You probably don't need this hook, because [`useBreakpoints()`](#usebreakpoints) abstracts the above implementation away for your convenience. You'll likely only need this hook if you need a property from `ResizeObserverEntry` which is not `contentRect` or one of `box`'s options.
+⚠️ **Hint** — You probably don't need this hook, because [`useBreakpoints()`](#usebreakpoints) abstracts the above implementation away for your convenience. You'll likely only need this hook if you need a property from `ResizeObserverEntry` which is not `contentRect` or one of the `ResizeObserverBoxOptions`.
---
@@ -538,23 +548,25 @@ const MyResponsiveComponent = () => {
## Reference guide
-```javascript
+```jsx
```
## Usage
`parent.js`
-```javascript
+
+```jsx
import { Context } from '@envato/react-breakpoints';
- /* children with access to `myResizeObserverEntry` */
-
+ {/* children with access to `myResizeObserverEntry` */}
+;
```
`child.js`
-```javascript
+
+```jsx
import { useContext } from 'react';
import { Context } from '@envato/react-breakpoints';
diff --git a/docs/boxSizes.md b/docs/boxSizes.md
new file mode 100644
index 0000000..effa7fe
--- /dev/null
+++ b/docs/boxSizes.md
@@ -0,0 +1,71 @@
+# Observing vs. Consuming `ResizeObserverSize`
+
+There is an important distinction between the `boxSize` you observe and the `boxSize` you pass to your breakpoints.
+
+> You can observe a `boxSize` on an element, and then respond to changes in **another** `boxSize` of that same element.
+
+There might be cases where observing the same box you're matching breakpoints against is not desirable. **This package supports observing and consuming different boxes on the same element.**
+
+
+What's happening under the hood?
+
+Consider this example code and chain of events:
+
+```jsx
+import { Observe, useBreakpoints } from '@envato/react-breakpoints';
+
+const MyObservedElementComponent = () => (
+ /* observed changes to `contentBoxSize` will update Observe's context */
+
+ {({ observedElementProps }) => (
+
This element is currently within the {label} range.
;
+};
+```
+
+1. You start observing an element's `contentBoxSize`. `` puts a `ResizeObserverEntry` on its context. **This object contains all box sizes of the observed element.**
+1. `` is aware of **all of the element's box sizes** via ``'s context. You decide you want to apply your breakpoints using the `devicePixelContentBoxSize` information.
+1. A moment later, the element's `contentBoxSize` changes.
+1. `` updates the `ResizeObserverEntry` on its context, and `` responds accordingly.
+1. Then, the element's `borderBoxSize` changes, but **not** its `contentBoxSize` (for example, when a CSS animation adds additional padding to an element).
+1. Because `borderBoxSize` is not observed, ``'s context does not get updated, and therefore `` does not update.
+1. Finally, after a while longer, the element's `devicePixelContentBoxSize` changes.
+1. Even though `` uses this box's size, `` is not observing changes on this box, and does not update its context to inform ``.
+
+
+# ⚠️ Important
+
+A change in one given `boxSize` does not always mean that the element's other `boxSize`s have also changed.
+
+Consider the [CSS box model](https://en.wikipedia.org/wiki/CSS_box_model#/media/File:Boxmodell-detail.png):
+
+
+
+When padding or border increase in thickness, the content's size will remain unaffected. If you are observing changes in `contentBoxSize`, those padding and border changes **will not trigger any updates**.
+
+Similarly, let's say you have the following element:
+
+```html
+
...
+```
+
+Then you change that element's `padding` to `0`. If you are observing changes in this element's `borderBoxSize`, this padding change **will not trigger any updates** because the `borderBoxSize` did not change.
diff --git a/docs/boxes.md b/docs/boxes.md
deleted file mode 100644
index d411505..0000000
--- a/docs/boxes.md
+++ /dev/null
@@ -1,78 +0,0 @@
-# Observing vs. Consuming boxes
-
-There is an important distinction between the `box` you observe and the `box` you consume for triggering breakpoints.
-
-> You can observe a box on an element, and then respond to changes in **another** box of that element.
-
-There might be cases where observing the same box you're matching breakpoints against is not desirable. **This package supports observing and consuming different boxes on the same element.**
-
-
-What's happening under the hood?
-
-
-Consider this example code and chain of events:
-
-```javascript
-import { Observe, useBreakpoints } from '@envato/react-breakpoints';
-
-const MyObservedElementComponent = () => (
- (
-
- This element is currently within the {label} range.
-
- );
-};
-```
-
-1. You start observing an element's `content-box` size. `` puts a `ResizeObserverEntry` on its context. **This object contains all box sizes of the observed element.**
-1. `` is aware of **all of the element's box sizes** via ``'s context. You decide you want to apply your breakpoints using the `device-pixel-content-box` information.
-1. A moment later, the element's `content-box` size changes.
-1. `` updates the `ResizeObserverEntry` on its context, and `` responds accordingly.
-1. Then, the element's `border-box` size changes, but **not** its `content-box`.
-1. Because `border-box` is not observed, ``'s context does not get updated, and therefore `` does not update.
-1. Finally, after a while longer, the element's `device-pixel-content-box` size changes.
-1. Even though `` uses this box's size, `` is not observing changes on this box, and does not update its context to inform ``.
-
-
-# ⚠️ Important
-
-A change in the size of one given `box` does not always mean that the element's other `box`es have also changed size.
-
-Consider the [CSS box model](https://en.wikipedia.org/wiki/CSS_box_model#/media/File:Boxmodell-detail.png):
-
-
-
-When padding or border increase in thickness, the content's size will remain unaffected. If you are observing changes in `content-box`'s size, those padding and border changes **will not trigger any updates**.
-
-Similarly, let's say you have the following element:
-
-```html
-
- ...
-
-```
-
-Then you change that element's `padding` to `0`. If you are observing changes in this element's `border-box` size, this padding change **will not trigger any updates** because the `border-box` did not change size.
diff --git a/docs/server-side-rendering.md b/docs/server-side-rendering.md
index 65b5c2a..16b75ef 100644
--- a/docs/server-side-rendering.md
+++ b/docs/server-side-rendering.md
@@ -2,7 +2,15 @@
The `widthMatch` and `heightMatch` values returned from [``](api.md#observe) and [`useBreakpoints()`](api.md#usebreakpoints) default to `undefined`. This is the case when:
-* the observed min-size isn't specified in your [`options`](api.md#usebreakpoints);
-* rendering a component server-side.
+- the observed min-size isn't specified in your [`options`](api.md#usebreakpoints);
+- rendering a component server-side.
-You can use this `undefined` value to display your component differently for SSR purposes. How you do it is up to you (loading component, default CSS styles, placeholder content, `null`, etc).
+You can use this `undefined` value to display your component differently for SSR purposes. How you do it is up to you (loading spinner component, default CSS styles, placeholder content, `null`, etc).
+
+# ⚠️ Beware of [Cumulative Layout Shift](https://web.dev/cls/)
+
+Remember, this is a JavaScript solution to a CSS problem. If you use React Breakpoints to apply different styles to your component based on their size, and you are rendering some HTML server-side, you may end up applying styles to your components that do not match their computed styles once JavaScript loads on the client. This means you could introduce a flash of incorrectly styled content (FOISC?).
+
+Unfortunately, there is no easy way around it: this is the nature of the beast. If you care deeply about [CLS](https://web.dev/cls/) (and for public pages you probably should!), you need to keep this side-effect in mind.
+
+However, React Breakpoints truly shines when you're building highly-responsive dashboards with graphs and tables, each individually responsive and aware of their own sizes. Dashboards for signed-in users generally don't suffer from SEO penalties like CLS, because they are not indexed by search engines.
diff --git a/package.json b/package.json
index 1fe2a74..254bc58 100644
--- a/package.json
+++ b/package.json
@@ -3,9 +3,8 @@
"version": "1.0.3",
"description": "Respond to changes in a DOM element's size. With React Breakpoints, element queries are no longer \"web design's unicorn\" 🦄",
"main": "dist/index.js",
- "types": "src/index.d.ts",
"scripts": {
- "build": "babel src/ -d dist/ --source-maps",
+ "build": "tsc -b",
"prepare": "npm run build"
},
"repository": {
@@ -13,6 +12,11 @@
"url": "git+https://github.com/envato/react-breakpoints.git"
},
"keywords": [
+ "resize-observer",
+ "media-queries",
+ "element-queries",
+ "container-queries",
+ "breakpoints",
"react-hooks",
"hooks",
"react",
@@ -28,18 +32,52 @@
},
"homepage": "https://github.com/envato/react-breakpoints#readme",
"peerDependencies": {
- "react": "^16.12.0",
- "react-dom": "^16.12.0"
+ "react": "16.8 - 17",
+ "react-dom": "16.8 - 17"
},
"devDependencies": {
- "@babel/cli": "^7.8.4",
- "@babel/core": "^7.8.6",
- "@babel/preset-env": "^7.7.7",
- "@babel/preset-react": "^7.8.3",
- "react": "^16.12.0",
- "react-dom": "^16.12.0"
+ "@types/react": "^17.0.0",
+ "@typescript-eslint/eslint-plugin": "^4.13.0",
+ "@typescript-eslint/parser": "^4.13.0",
+ "babel-eslint": "^10.1.0",
+ "eslint": "^7.17.0",
+ "eslint-config-react-app": "^6.0.0",
+ "eslint-plugin-flowtype": "^5.2.0",
+ "eslint-plugin-import": "^2.22.1",
+ "eslint-plugin-jsx-a11y": "^6.4.1",
+ "eslint-plugin-react": "^7.22.0",
+ "eslint-plugin-react-hooks": "^4.2.0",
+ "husky": "^4.3.7",
+ "lint-staged": "^10.5.3",
+ "prettier": "^2.2.1",
+ "react": "^17.0.1",
+ "react-dom": "^17.0.1",
+ "typescript": "^4.2.2"
},
"dependencies": {
- "@envato/react-resize-observer-hook": "^1.0.1"
+ "@envato/react-resize-observer-hook": "^1.2.0"
+ },
+ "eslintConfig": {
+ "extends": "react-app"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "lint-staged"
+ }
+ },
+ "lint-staged": {
+ "**/*.{js,ts,tsx}": [
+ "eslint --cache --fix",
+ "prettier --write"
+ ]
+ },
+ "prettier": {
+ "arrowParens": "avoid",
+ "jsxSingleQuote": true,
+ "printWidth": 120,
+ "quoteProps": "preserve",
+ "semi": true,
+ "singleQuote": true,
+ "trailingComma": "none"
}
}
diff --git a/src/Breakpoints.ts b/src/Breakpoints.ts
new file mode 100644
index 0000000..8eae541
--- /dev/null
+++ b/src/Breakpoints.ts
@@ -0,0 +1,3 @@
+export interface Breakpoints {
+ [breakpoint: number]: any;
+}
diff --git a/src/Context.js b/src/Context.js
deleted file mode 100644
index d8de05e..0000000
--- a/src/Context.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { createContext } from 'react';
-
-const Context = createContext(null);
-
-export { Context };
diff --git a/src/Context.ts b/src/Context.ts
new file mode 100644
index 0000000..0eb43dd
--- /dev/null
+++ b/src/Context.ts
@@ -0,0 +1,4 @@
+import { createContext } from 'react';
+import { ExtendedResizeObserverEntry } from '@envato/react-resize-observer-hook';
+
+export const Context = createContext(null);
diff --git a/src/Observe.js b/src/Observe.js
deleted file mode 100644
index bf2d090..0000000
--- a/src/Observe.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import React from 'react';
-import { useResizeObserver } from '@envato/react-resize-observer-hook';
-import { Context } from './Context';
-import { useBreakpoints } from './useBreakpoints';
-
-const Observe = ({
- box = undefined,
- breakpoints = {},
- render
-}) => {
- const observeOptions = box ? { box } : {};
-
- const [ref, observedEntry] = useResizeObserver(observeOptions);
- const [widthMatch, heightMatch] = useBreakpoints(breakpoints, observedEntry);
-
- const renderOptions = {
- observedElementProps: { ref },
- widthMatch,
- heightMatch
- };
-
- return (
-
- {render(renderOptions)}
-
- );
-};
-
-export { Observe };
diff --git a/src/Observe.tsx b/src/Observe.tsx
new file mode 100644
index 0000000..51f6248
--- /dev/null
+++ b/src/Observe.tsx
@@ -0,0 +1,51 @@
+import { ReactNode } from 'react';
+import { ObservedElement, useResizeObserver } from '@envato/react-resize-observer-hook';
+import { Context } from './Context';
+import { UseBreakpointsOptions, useBreakpoints } from './useBreakpoints';
+
+interface ObservedElementProps {
+ ref: React.RefCallback;
+}
+
+interface RenderOptions {
+ observedElementProps: ObservedElementProps;
+ widthMatch: any;
+ heightMatch: any;
+}
+
+interface BaseObserveProps {
+ box?: ResizeObserverBoxOptions;
+ breakpoints?: UseBreakpointsOptions;
+}
+
+interface ObserveViaRenderProp extends BaseObserveProps {
+ children?: never;
+ render: (args: RenderOptions) => ReactNode;
+}
+
+interface ObserveViaChildrenProp extends BaseObserveProps {
+ children: (args: RenderOptions) => ReactNode;
+ render?: never;
+}
+
+/* Allow use of `render` or `children` props, but not both. */
+type ObserveProps = ObserveViaRenderProp | ObserveViaChildrenProp;
+
+export const Observe = ({ box = undefined, breakpoints, children, render }: ObserveProps): JSX.Element => {
+ const observeOptions = box ? { box } : {};
+
+ const [ref, observedEntry] = useResizeObserver(observeOptions);
+ const { widthMatch, heightMatch } = useBreakpoints(breakpoints, observedEntry);
+
+ const renderOptions: RenderOptions = {
+ observedElementProps: { ref },
+ widthMatch,
+ heightMatch
+ };
+
+ return (
+
+ {children ? children(renderOptions) : render ? render(renderOptions) : null}
+
+ );
+};
diff --git a/src/findBreakpoint.ts b/src/findBreakpoint.ts
new file mode 100644
index 0000000..43f67d8
--- /dev/null
+++ b/src/findBreakpoint.ts
@@ -0,0 +1,31 @@
+import { Breakpoints } from './Breakpoints';
+
+/**
+ * From a `breakpoints` object, find the first key that is equal to or greater than
+ * `observedSize` and return the corresponding value.
+ *
+ * @example
+ * const widths = {
+ * 0: 'mobile',
+ * 769: 'laptop',
+ * 1025: 'desktop',
+ * 1441: 'widescreen'
+ * };
+ *
+ * const matchedValue = findBreakpoint(widths, 1280);
+ * // matchedValue => 'desktop'
+ *
+ */
+export const findBreakpoint = (breakpoints: Breakpoints, observedSize?: number): any => {
+ if (typeof observedSize === 'undefined') return undefined;
+
+ let breakpoint: number | undefined;
+ const sizes = Object.keys(breakpoints).map(key => Number(key));
+
+ for (const next of sizes) {
+ if (observedSize < next) break;
+ breakpoint = next;
+ }
+
+ return typeof breakpoint === 'undefined' ? undefined : breakpoints[breakpoint];
+};
diff --git a/src/index.d.ts b/src/index.d.ts
deleted file mode 100644
index fa17268..0000000
--- a/src/index.d.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react';
-
-type BoxOptions = 'border-box' | 'content-box' | 'device-pixel-content-box';
-
-interface ResizeObserverBoxSize {
- readonly inlineSize: number;
- readonly blockSize: number;
-}
-
-interface ResizeObserverEntry {
- readonly target: Element;
- readonly contentRect: DOMRectReadOnly;
- readonly borderBoxSize: Array | ResizeObserverBoxSize;
- readonly contentBoxSize: Array | ResizeObserverBoxSize;
- readonly devicePixelContentBoxSize: Array | ResizeObserverBoxSize;
-}
-
-interface Breakpoints {
- [key: number]: any;
-}
-
-interface BreakpointsOptions {
- box?: BoxOptions;
- widths?: Breakpoints;
- heights?: Breakpoints;
- fragment?: number;
-}
-
-interface ObservedElementProps {
- ref: () => React.RefObject;
-}
-
-interface ObserveRenderArgs {
- observedElementProps: ObservedElementProps;
- widthMatch: any;
- heightMatch: any;
-}
-
-interface ObserveProps {
- box?: BoxOptions;
- breakpoints?: BreakpointsOptions;
- render: ({
- observedElementProps,
- widthMatch,
- heightMatch
- }: ObserveRenderArgs) => React.ReactNode;
-}
-
-export const Observe: (props: ObserveProps) => React.ReactNode;
-
-export const useBreakpoints: (options: BreakpointsOptions, injectResizeObserverEntry?: ResizeObserverEntry) => [any, any];
-
-export const useResizeObserverEntry: (injectResizeObserverEntry?: ResizeObserverEntry) => ResizeObserverEntry | null;
-
-export const Context: React.Context;
diff --git a/src/index.js b/src/index.ts
similarity index 100%
rename from src/index.js
rename to src/index.ts
diff --git a/src/useBreakpoints.js b/src/useBreakpoints.js
deleted file mode 100644
index 9f66823..0000000
--- a/src/useBreakpoints.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import { useResizeObserverEntry } from './useResizeObserverEntry';
-
-const boxOptions = {
- BORDER_BOX: 'border-box', // https://caniuse.com/#feat=mdn-api_resizeobserverentry_borderboxsize
- CONTENT_BOX: 'content-box', // https://caniuse.com/#feat=mdn-api_resizeobserverentry_contentboxsize
- DEVICE_PIXEL_CONTENT_BOX: 'device-pixel-content-box' // https://github.com/w3c/csswg-drafts/issues/3554
-};
-
-/**
- * Find the breakpoint matching the given entry size.
- * @argument {Object} breakpoints - Map of sizes with their return values.
- * @argument {Number} entrySize - Size of entry to check breakpoints against.
- * @returns {*} Value of `breakpoints` at matching breakpoint.
- */
-const findBreakpoint = (breakpoints, entrySize) => {
- if (typeof entrySize === 'undefined') return undefined;
-
- let breakpoint;
- const sizes = Object.keys(breakpoints);
-
- for (const next of sizes) {
- if (entrySize < Number(next)) break;
- breakpoint = next;
- }
-
- return breakpoints[breakpoint];
-};
-
-/**
- * Returns mapped value for width and height of nearest Context.
- * @argument {Object} options
- * @argument {Object} [options.widths] - Map of minWidths and their return values.
- * @argument {Object} [options.heights] - Map of minHeights and their return values.
- * @argument {String} [options.box] - Name of element's box you want to match your breakpoints against. One of ['border-box', 'content-box', 'device-pixel-content-box'].
- * @argument {Number} [options.fragment] - Index of fragment to return from array of observed box fragments.
- * @argument {ResizeObserverEntry} [injectResizeObserverEntry] - Explicitly set the ResizeObserverEntry to use instead of fetching it from Context.
- * @returns {Array} Array of matching width value, and matching height value.
- */
-const useBreakpoints = ({
- widths = {},
- heights = {},
- box = undefined,
- fragment = 0 // https://github.com/w3c/csswg-drafts/pull/4529
-}, injectResizeObserverEntry = undefined) => {
- const resizeObserverEntry = useResizeObserverEntry(injectResizeObserverEntry);
-
- let entryBox, entryWidth, entryHeight;
-
- if (resizeObserverEntry) {
- switch (box) {
- case boxOptions.BORDER_BOX:
- entryBox = resizeObserverEntry.borderBoxSize[fragment] || resizeObserverEntry.borderBoxSize;
- entryWidth = entryBox.inlineSize;
- entryHeight = entryBox.blockSize;
- break;
-
- case boxOptions.CONTENT_BOX:
- entryBox = resizeObserverEntry.contentBoxSize[fragment] || resizeObserverEntry.contentBoxSize;
- entryWidth = entryBox.inlineSize;
- entryHeight = entryBox.blockSize;
- break;
-
- case boxOptions.DEVICE_PIXEL_CONTENT_BOX:
- entryBox = resizeObserverEntry.devicePixelContentBoxSize[fragment] || resizeObserverEntry.devicePixelContentBoxSize;
- entryWidth = entryBox.inlineSize;
- entryHeight = entryBox.blockSize;
- break;
-
- default:
- entryWidth = resizeObserverEntry.contentRect.width;
- entryHeight = resizeObserverEntry.contentRect.height;
- }
- }
-
- const widthMatch = findBreakpoint(widths, entryWidth);
- const heightMatch = findBreakpoint(heights, entryHeight);
-
- return [widthMatch, heightMatch];
-};
-
-export { useBreakpoints };
diff --git a/src/useBreakpoints.ts b/src/useBreakpoints.ts
new file mode 100644
index 0000000..41e6c3a
--- /dev/null
+++ b/src/useBreakpoints.ts
@@ -0,0 +1,135 @@
+import { useRef, useState, useEffect, useMemo } from 'react';
+import { ExtendedResizeObserverEntry } from '@envato/react-resize-observer-hook';
+import { Breakpoints } from './Breakpoints';
+import { useResizeObserverEntry } from './useResizeObserverEntry';
+import { findBreakpoint } from './findBreakpoint';
+
+interface BaseOptions {
+ box?: ResizeObserverBoxOptions;
+ fragment?: number;
+}
+
+interface HeightsOptions extends BaseOptions {
+ widths?: Breakpoints;
+ heights: Breakpoints;
+}
+
+interface WidthsOptions extends BaseOptions {
+ widths: Breakpoints;
+ heights?: Breakpoints;
+}
+
+export type UseBreakpointsOptions = HeightsOptions | WidthsOptions;
+
+type Matches = { widthMatch: any; heightMatch: any };
+
+export type UseBreakpointsResult = [any, any] & Matches;
+
+const boxOptions = {
+ BORDER_BOX: 'border-box', // https://caniuse.com/mdn-api_resizeobserverentry_borderboxsize
+ CONTENT_BOX: 'content-box', // https://caniuse.com/mdn-api_resizeobserverentry_contentboxsize
+ DEVICE_PIXEL_CONTENT_BOX: 'device-pixel-content-box' // https://github.com/w3c/csswg-drafts/pull/4476
+};
+
+/**
+ * See API Docs: {@linkcode https://github.com/envato/react-breakpoints/blob/master/docs/api.md#usebreakpoints|useBreakpoints}
+ *
+ * Pass in an options object with at least one of the following properties:
+ * - `widths`: objects with width breakpoints as keys and anything as their values;
+ * - `heights`: objects with height breakpoints as keys and anything as their values.
+ *
+ * You may also pass the following additional optional properties:
+ * - `box`: the box to measure on the observed element, one of `'border-box' | 'content-box' | 'device-pixel-content-box'`;
+ * - `fragment`: index of {@link https://github.com/w3c/csswg-drafts/pull/4529|fragment} of the observed element to measure (default `0`).
+ *
+ * Optionally pass in a `ResizeObserverEntry` as the second argument to override fetching one from context.
+ *
+ * @example
+ * const options = {
+ * widths: {
+ * 0: 'mobile',
+ * 769: 'tablet',
+ * 1025: 'desktop'
+ * }
+ * };
+ *
+ * const { widthMatch: label } = useBreakpoints(options);
+ *
+ * return (
+ *
+ * This element is currently within the {label} range.
+ *
+ * );
+ */
+export const useBreakpoints = (
+ {
+ widths = {},
+ heights = {},
+ box = undefined,
+ fragment = 0 // https://github.com/w3c/csswg-drafts/pull/4529
+ }: UseBreakpointsOptions = { widths: {}, heights: {} },
+ injectResizeObserverEntry?: ExtendedResizeObserverEntry | null
+) => {
+ const isMounted = useRef(true);
+ const matches = useRef({ widthMatch: undefined, heightMatch: undefined });
+ const [changedMatches, changeMatches] = useState({ widthMatch: undefined, heightMatch: undefined });
+
+ /* Prevent further observation state changes if component is no longer mounted. */
+ useEffect(() => {
+ return () => {
+ isMounted.current = false;
+ };
+ }, []);
+
+ const result = useMemo(() => {
+ /* Support both array and object destructuring. */
+ const result = [changedMatches.widthMatch, changedMatches.heightMatch] as UseBreakpointsResult;
+ result.widthMatch = changedMatches.widthMatch;
+ result.heightMatch = changedMatches.heightMatch;
+
+ return result;
+ }, [changedMatches.widthMatch, changedMatches.heightMatch]);
+
+ const resizeObserverEntry = useResizeObserverEntry(injectResizeObserverEntry);
+
+ if (!resizeObserverEntry) return result;
+
+ let observedBoxSize: ResizeObserverSize;
+
+ switch (box) {
+ case boxOptions.BORDER_BOX:
+ observedBoxSize = resizeObserverEntry.borderBoxSize[fragment];
+ break;
+
+ case boxOptions.CONTENT_BOX:
+ observedBoxSize = resizeObserverEntry.contentBoxSize[fragment];
+ break;
+
+ case boxOptions.DEVICE_PIXEL_CONTENT_BOX:
+ if (typeof resizeObserverEntry.devicePixelContentBoxSize !== 'undefined') {
+ observedBoxSize = resizeObserverEntry.devicePixelContentBoxSize[fragment];
+ } else {
+ throw Error('resizeObserverEntry does not contain devicePixelContentBoxSize.');
+ }
+ break;
+
+ default:
+ observedBoxSize = {
+ inlineSize: resizeObserverEntry.contentRect.width,
+ blockSize: resizeObserverEntry.contentRect.height
+ };
+ }
+
+ const widthMatch = findBreakpoint(widths, observedBoxSize.inlineSize);
+ const heightMatch = findBreakpoint(heights, observedBoxSize.blockSize);
+
+ if (widthMatch !== matches.current.widthMatch || heightMatch !== matches.current.heightMatch) {
+ matches.current = { widthMatch, heightMatch };
+
+ if (isMounted.current) {
+ changeMatches({ widthMatch, heightMatch });
+ }
+ }
+
+ return result;
+};
diff --git a/src/useResizeObserverEntry.js b/src/useResizeObserverEntry.js
deleted file mode 100644
index 313c9b4..0000000
--- a/src/useResizeObserverEntry.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { useContext } from 'react';
-import { Context } from './Context';
-
-/**
- * Returns the ResizeObserverEntry from nearest Context.
- * @argument {ResizeObserverEntry} [injectResizeObserverEntry] - Explicitly set the ResizeObserverEntry to use instead of fetching it from Context.
- * @returns {(ResizeObserverEntry|null)}
- */
-const useResizeObserverEntry = injectResizeObserverEntry => {
- const resizeObserverEntry = useContext(Context);
-
- return injectResizeObserverEntry || resizeObserverEntry;
-};
-
-export { useResizeObserverEntry };
diff --git a/src/useResizeObserverEntry.ts b/src/useResizeObserverEntry.ts
new file mode 100644
index 0000000..7dbb46c
--- /dev/null
+++ b/src/useResizeObserverEntry.ts
@@ -0,0 +1,40 @@
+import { useContext } from 'react';
+import { ExtendedResizeObserverEntry } from '@envato/react-resize-observer-hook';
+import { Context } from './Context';
+
+/**
+ * See API Docs: {@linkcode https://github.com/envato/react-breakpoints/blob/master/docs/api.md#useresizeobserverentry|useResizeObserverEntry}
+ *
+ * Returns the `ResizeObserverEntry` from the nearest Context.
+ *
+ * You can also pass in a `ResizeObserverEntry`, which will be returned verbatim.
+ * You will almost certainly never need to do this, but because you may not
+ * conditionally call hooks, it can be useful to pass in the `ResizeObserverEntry`
+ * you receive from
+ * {@linkcode https://github.com/envato/react-breakpoints/blob/master/docs/api.md#useresizeobserver|useResizeObserver}
+ * in the same component instead of relying on the value from the nearest Context.
+ *
+ * This is allowed to facilitate the abstraction in
+ * {@linkcode https://github.com/envato/react-breakpoints/blob/master/docs/api.md#usebreakpoints|useBreakpoints}
+ * which is used in the
+ * {@linkcode https://github.com/envato/react-breakpoints/blob/master/docs/api.md#observe|Observe}
+ * component.
+ *
+ * @example
+ * // MyObservingComponent
+ * return (
+ *
+ *
+ *
+ * );
+ *
+ * // MyConsumingComponent
+ * const someResizeObserverEntry = useResizeObserverEntry();
+ */
+export const useResizeObserverEntry = (
+ injectResizeObserverEntry?: ExtendedResizeObserverEntry | null
+): ExtendedResizeObserverEntry | null => {
+ const resizeObserverEntry = useContext(Context);
+
+ return injectResizeObserverEntry || resizeObserverEntry;
+};
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..52f5d32
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "module": "commonjs",
+ "jsx": "react-jsx",
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "outDir": "./dist",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["./src/**/*"]
+}