├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmrc ├── .prettierignore ├── .size-snapshot.json ├── .travis.yml ├── CHANGES.md ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── jest.config.js ├── modules ├── .babelrc ├── Media.js ├── MediaQueryListener.js ├── __tests__ │ ├── .eslintrc │ ├── Media-ssr-test.js │ ├── Media-test.js │ └── utils.js └── index.js ├── package.json ├── rollup.config.js └── typeTest.tsx /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true 5 | }, 6 | "plugins": ["import", "react", "react-hooks"], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:import/errors", 10 | "plugin:react/recommended", 11 | "plugin:react-hooks/recommended" 12 | ], 13 | "globals": { 14 | "__DEV__": true 15 | }, 16 | "settings": { 17 | "react": { 18 | "version": "16" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | 3 | /cjs/ 4 | /esm/ 5 | /umd/ 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | message="Version %s" 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "esm/react-media.js": { 3 | "bundled": 7913, 4 | "minified": 3466, 5 | "gzipped": 1246, 6 | "treeshaked": { 7 | "rollup": { 8 | "code": 364, 9 | "import_statements": 187 10 | }, 11 | "webpack": { 12 | "code": 3689 13 | } 14 | } 15 | }, 16 | "umd/react-media.js": { 17 | "bundled": 38897, 18 | "minified": 11948, 19 | "gzipped": 4235 20 | }, 21 | "umd/react-media.min.js": { 22 | "bundled": 15270, 23 | "minified": 5110, 24 | "gzipped": 2190 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: node 3 | cache: yarn 4 | env: 5 | - TEST_ENV=cjs BUILD_ENV=cjs 6 | - TEST_ENV=umd BUILD_ENV=umd 7 | - TEST_ENV=source 8 | before_script: 9 | - ([[ -z "$BUILD_ENV" ]] || npm run build) 10 | script: 11 | - yarn run lint 12 | - yarn test 13 | - yarn run testTypes 14 | jobs: 15 | include: 16 | - stage: Release 17 | if: tag =~ ^v[0-9] 18 | env: NPM_TAG=$([[ "$TRAVIS_TAG" == *-* ]] && echo "next" || echo "latest") 19 | script: echo "Releasing $TRAVIS_TAG to npm with tag \"$NPM_TAG\" ..." 20 | deploy: 21 | provider: npm 22 | skip_cleanup: true 23 | tag: "$NPM_TAG" 24 | email: npm@mjackson.me 25 | api_key: "$NPM_TOKEN" 26 | on: 27 | tags: true 28 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## [v1.9.0] 2 | > Dec 12, 2018 3 | 4 | - Add Typescript definitions [#76](https://github.com/ReactTraining/react-media/pull/76) 5 | - Add `onChange` callback [#95](https://github.com/ReactTraining/react-media/pull/95) 6 | - Add workaround for Safari not clearing up event handlers properly [#101](https://github.com/ReactTraining/react-media/pull/101) 7 | 8 | [v1.9.0]: https://github.com/ReactTraining/react-media/compare/v1.7.0...v1.9.0 9 | 10 | ## [v1.7.0] 11 | > Feb 1, 2018 12 | 13 | - Add optional `targetWindow` prop [#78](https://github.com/ReactTraining/react-media/pull/78) 14 | 15 | [v1.7.0]: https://github.com/ReactTraining/react-media/compare/v1.6.0...v1.7.0 16 | 17 | ## [v1.6.0] 18 | > Jul 11, 2017 19 | 20 | - Add support for multiple queries [#39](https://github.com/ReactTraining/react-media/pull/39) 21 | - Add optional `defaultMatches` prop [#50](https://github.com/ReactTraining/react-media/pull/50) 22 | 23 | [v1.6.0]: https://github.com/ReactTraining/react-media/compare/v1.5.0...v1.6.0 24 | 25 | ## [v1.5.0] 26 | > Feb 17, 2017 27 | 28 | - Add Preact support 29 | - Add ES modules build 30 | 31 | [v1.5.0]: https://github.com/ReactTraining/react-media/compare/v1.4.0...v1.5.0 32 | 33 | ## [v1.4.0] 34 | > Dec 14, 2016 35 | 36 | - Compatibility with React 0.14 37 | 38 | [v1.4.0]: https://github.com/ReactTraining/react-media/compare/v1.3.2...v1.4.0 39 | 40 | ## [v1.3.2] 41 | > Oct 10, 2016 42 | 43 | - Fixed missing `json2mq` dependency ([#20]) 44 | 45 | [v1.3.2]: https://github.com/ReactTraining/react-media/compare/v1.3.0...v1.3.2 46 | [#20]: https://github.com/ReactTraining/react-media/pull/20 47 | 48 | ## [v1.3.0] 49 | > Oct 4, 2016 50 | 51 | - Added support for objects in `` ([#18]) 52 | 53 | [v1.3.0]: https://github.com/ReactTraining/react-media/compare/v1.2.2...v1.3.0 54 | [#18]: https://github.com/ReactTraining/react-media/pull/18 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2017 React Training 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-media [![Travis][build-badge]][build] [![npm package][npm-badge]][npm] 2 | 3 | [build-badge]: https://img.shields.io/travis/ReactTraining/react-media/master.svg?style=flat-square 4 | [build]: https://travis-ci.org/ReactTraining/react-media 5 | [npm-badge]: https://img.shields.io/npm/v/react-media.svg?style=flat-square 6 | [npm]: https://www.npmjs.org/package/react-media 7 | 8 | [`react-media`](https://www.npmjs.com/package/react-media) is a CSS media query component for React. 9 | 10 | A `` component listens for matches to a [CSS media query](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries) and renders stuff based on whether the query matches or not. 11 | 12 | ## Installation 13 | 14 | Using npm: 15 | 16 | $ npm install --save react-media 17 | 18 | Then, use as you would anything else: 19 | 20 | ```js 21 | // using ES modules 22 | import Media from 'react-media'; 23 | 24 | // using CommonJS modules 25 | var Media = require('react-media'); 26 | ``` 27 | 28 | The UMD build is also available on [unpkg](https://unpkg.com): 29 | 30 | ```html 31 | 32 | ``` 33 | 34 | You can find the library on `window.ReactMedia`. 35 | 36 | ## Hooks are coming soon! 37 | 38 | Hooks are available in 2.X branch. 39 | 40 | Install `react-media@next` to get it. 41 | 42 | `useMedia` accepts a single options argument to handle both single and multiple queries, so the same properties as Media are available (except of course render and children props). 43 | 44 | 45 | Simple usage with multiple queries: 46 | 47 | ```tsx 48 | import { useMedia } from 'react-media'; 49 | 50 | const GLOBAL_MEDIA_QUERIES = { 51 | small: "(max-width: 599px)", 52 | medium: "(min-width: 600px) and (max-width: 1199px)", 53 | large: "(min-width: 1200px)" 54 | }; 55 | const matches = useMedia({ queries: GLOBAL_MEDIA_QUERIES }); 56 | 57 | const marginBottom = matches.large ? 0 : 10; 58 | ``` 59 | 60 | With single query : 61 | 62 | ```tsx 63 | import { useMedia } from 'react-media'; 64 | 65 | const isSmallScreen = useMedia({ query: "(max-width: 599px)" }); 66 | 67 | ``` 68 | 69 | 70 | ## Basic usage 71 | 72 | ### queries 73 | 74 | Render a `` component with a `queries` prop whose value is an object, 75 | where each value is a valid 76 | [CSS media query](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries). 77 | The `children` prop should be a function whose argument will be an object with the 78 | same keys as your `queries` object, and whose values are booleans indicating whether 79 | each query matches. 80 | 81 | ```jsx 82 | import React, { Fragment } from 'react'; 83 | import Media from 'react-media'; 84 | 85 | class App extends React.Component { 86 | render() { 87 | return ( 88 |
89 | 94 | {matches => ( 95 | 96 | {matches.small &&

I am small!

} 97 | {matches.medium &&

I am medium!

} 98 | {matches.large &&

I am large!

} 99 |
100 | )} 101 |
102 |
103 | ); 104 | } 105 | } 106 | ``` 107 | 108 | ### query 109 | 110 | Alternatively, if you only need to match against a single media query, the `query` prop provides a less-verbose approach. 111 | More documentation about the difference between `query` and `queries` can be found below. 112 | 113 | ```jsx 114 | import React, { Fragment } from 'react'; 115 | import Media from 'react-media'; 116 | 117 | class App extends React.Component { 118 | render() { 119 | return ( 120 |
121 | 122 | ( 123 |

I am small!

124 | )} 125 | /> 126 |
127 | ); 128 | } 129 | } 130 | ``` 131 | 132 | ## `query` vs `queries` 133 | 134 | The `queries` prop was added to allow for multiple media queries to be matched without excessive nesting or other 135 | workarounds. The `query` prop was retained out of recognition that a single query covers many use cases, and there 136 | is already a lot of usage that would be a pain to migrate. 137 | 138 | The salient points: 139 | 140 | * **You cannot use them together**: if you do, the component will throw an error. This is to avoid confusion around 141 | precedence. 142 | * **The render methods differ slightly**: for the `queries` prop, the `render` and child JSX methods will render if 143 | **at least one** of the given queries is matched. The `query` prop renders if the given query matches. 144 | 145 | 146 | ## `queries` 147 | 148 | In addition to passing a valid media query string, the `queries` 149 | prop will also accept an object of objects whose forms are similar to 150 | [React's built-in support for inline style objects](https://facebook.github.io/react/tips/inline-styles.html) 151 | in e.g. `
`. These objects are converted to CSS 152 | media queries via [json2mq](https://github.com/akiran/json2mq/blob/master/README.md#usage). 153 | 154 | ```jsx 155 | import React from 'react'; 156 | import Media from 'react-media'; 157 | 158 | class App extends React.Component { 159 | render() { 160 | return ( 161 |
162 |

These two Media components are equivalent

163 | 164 | 165 | {matches => 166 | matches.small ? ( 167 |

The document is less than 600px wide.

168 | ) : ( 169 |

The document is at least 600px wide.

170 | ) 171 | } 172 |
173 | 174 | 175 | {matches => 176 | matches.small ? ( 177 |

The document is less than 600px wide.

178 | ) : ( 179 |

The document is at least 600px wide.

180 | ) 181 | } 182 |
183 |
184 | ); 185 | } 186 | } 187 | ``` 188 | 189 | Keys of media query objects are camel-cased and numeric values automatically get the `px` suffix. See the [json2mq docs](https://github.com/akiran/json2mq/blob/master/README.md#usage) for more examples of queries you can construct using objects. 190 | 191 | ### Render props 192 | 193 | There are three props which allow you to render your content. They each serve a subtly different purpose. 194 | 195 | |prop|description|example| 196 | |---|---|---| 197 | |render|Only invoked when **at least one** of the queries matches. This is a nice shorthand if you only want to render something for a matching query.|`

I matched!

} />`| 198 | |children (function)|Receives an object of booleans whose keys are the same as the `queries` prop, indicating whether each media query matched. Use this prop if you need to render different output for each of specified queries.|`{matches => matches.foo ?

I matched!

:

I didn't match

}
`| 199 | |children (react element)|If you render a regular React element within ``, it will render that element when **at least one** of the queries matches. This method serves the same purpose as the `render` prop, however, you'll create component instances regardless of whether the queries match or not. Hence, using the `render` prop is preferred ([more info](https://github.com/ReactTraining/react-media/issues/70#issuecomment-347774260)).|`

I matched!

`| 200 | 201 | ## `query` 202 | 203 | In addition to passing a valid media query string, the `query` prop will also accept an object, similar to [React's built-in support for inline style objects](https://facebook.github.io/react/tips/inline-styles.html) in e.g. `
`. These objects are converted to CSS media queries via [json2mq](https://github.com/akiran/json2mq/blob/master/README.md#usage). 204 | 205 | ```jsx 206 | import React from 'react'; 207 | import Media from 'react-media'; 208 | 209 | class App extends React.Component { 210 | render() { 211 | return ( 212 |
213 |

These two Media components are equivalent

214 | 215 | 216 | {matches => 217 | matches ? ( 218 |

The document is less than 600px wide.

219 | ) : ( 220 |

The document is at least 600px wide.

221 | ) 222 | } 223 |
224 | 225 | 226 | {matches => 227 | matches ? ( 228 |

The document is less than 600px wide.

229 | ) : ( 230 |

The document is at least 600px wide.

231 | ) 232 | } 233 |
234 |
235 | ); 236 | } 237 | } 238 | ``` 239 | 240 | Keys of media query objects are camel-cased and numeric values automatically get the `px` suffix. See the [json2mq docs](https://github.com/akiran/json2mq/blob/master/README.md#usage) for more examples of queries you can construct using objects. 241 | 242 | ### Render props 243 | 244 | There are three props which allow you to render your content. They each serve a subtly different purpose. 245 | 246 | |prop|description|example| 247 | |---|---|---| 248 | |render|Only invoked when the query matches. This is a nice shorthand if you only want to render something for a matching query.|`

I matched!

} />`| 249 | |children (function)|Receives a single boolean element, indicating whether the media query matched. Use this prop if you need to render something when the query doesn't match.|`{matches => matches ?

I matched!

:

I didn't match

}
`| 250 | |children (react element)|If you render a regular React element within ``, it will render that element when the query matches. This method serves the same purpose as the `render` prop, however, you'll create component instances regardless of whether the query matches or not. Hence, using the `render` prop is preferred ([more info](https://github.com/ReactTraining/react-media/issues/70#issuecomment-347774260)).|`

I matched!

`| 251 | 252 | ## `onChange` 253 | 254 | You can specify an optional `onChange` prop, which is a callback function that will be invoked when the status of the media queries changes. This can be useful for triggering side effects, independent of the render lifecycle. 255 | 256 | ```jsx 257 | import React from 'react'; 258 | import Media from 'react-media'; 259 | 260 | class App extends React.Component { 261 | render() { 262 | return ( 263 |
264 | 267 | matches.small 268 | ? alert('The document is less than 600px wide.') 269 | : alert('The document is at least 600px wide.') 270 | } 271 | /> 272 |
273 | ); 274 | } 275 | } 276 | ``` 277 | 278 | ### Server-side rendering (SSR) 279 | 280 | If you render a `` component on the server, it will match by default. You can override the default behavior by setting the `defaultMatches` prop. 281 | 282 | When rendering on the server you can use the `defaultMatches` prop to set the initial state on the server to match whatever you think it will be on the client. You can detect the user's device [by analyzing the user-agent string](https://github.com/ReactTraining/react-media/pull/50#issuecomment-415700905) from the HTTP request in your server-side rendering code. 283 | 284 | ```js 285 | initialState = { 286 | device: 'mobile' // add your own guessing logic here, based on user-agent for example 287 | }; 288 | 289 |
290 | Render me below medium breakpoint.} 294 | /> 295 | 296 | Render me above medium breakpoint.} 300 | /> 301 |
; 302 | ``` 303 | 304 | ## `targetWindow` 305 | 306 | An optional `targetWindow` prop can be specified if you want the `queries` to be evaluated against a different window object than the one the code is running in. This can be useful if you are rendering part of your component tree to an iframe or [a popup window](https://hackernoon.com/using-a-react-16-portal-to-do-something-cool-2a2d627b0202). See [this PR thread](https://github.com/ReactTraining/react-media/pull/78) for context. 307 | 308 | ## About 309 | 310 | `react-media` is developed and maintained by [React Training](https://reacttraining.com). If you're interested in learning more about what React can do for your company, please [get in touch](mailto:hello@reacttraining.com)! 311 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | export interface MediaQueryObject { 3 | [id: string]: boolean | number | string; 4 | } 5 | 6 | /** 7 | * All allowed forms of media query inputs 8 | */ 9 | type MediaQueryValue = string | MediaQueryObject | MediaQueryObject[]; 10 | 11 | /** 12 | * The type of the `queries` prop 13 | */ 14 | interface MediaQueries { 15 | [key: string]: MediaQueryValue; 16 | } 17 | 18 | /** 19 | * The type of returned `matches` in case the `queries` prop is provided. The keys on `matches` 20 | * are inferred from the shape of `queries`. 21 | * 22 | * @example 23 | * 24 | * { 25 | * // matches: { small: boolean, medium: boolean } 26 | * matches => {} 27 | * } 28 | */ 29 | type QueryResults = { [key in keyof Queries]: boolean }; 30 | 31 | type BaseProps = { 32 | render?: () => React.ReactNode; 33 | targetWindow?: Window; 34 | }; 35 | 36 | /** 37 | * Props for the component when specifying `queries` (as opposed to `query`) 38 | */ 39 | export type MultiQueryProps = BaseProps & { 40 | queries: Queries; 41 | defaultMatches?: Partial>; 42 | children?: 43 | | ((matches: QueryResults) => React.ReactNode) 44 | | React.ReactNode; 45 | onChange?: (matches: QueryResults) => void; 46 | }; 47 | 48 | 49 | /** 50 | * Props for the component when specifying `query` (as opposed to `queries`) 51 | */ 52 | export type SingleQueryProps = BaseProps & { 53 | query: MediaQueryValue; 54 | defaultMatches?: boolean; 55 | children?: ((matches: boolean) => React.ReactNode) | React.ReactNode; 56 | onChange?: (matches: boolean) => void; 57 | }; 58 | 59 | /** 60 | * Conditionally renders based on whether or not a media query matches. 61 | */ 62 | export default function Media(props: SingleQueryProps): React.ReactElement; 63 | export default function Media(props: MultiQueryProps): React.ReactElement; 64 | 65 | type UseMediaSingleQueryProps = Omit 66 | type UseMediaMultiQueryProps = Omit, 'render' | 'children'> 67 | 68 | export function useMedia(props: SingleQueryProps): boolean; 69 | export function useMedia(props: UseMediaMultiQueryProps): QueryResults; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | module.exports = require('./cjs/react-media.min.js'); 5 | } else { 6 | module.exports = require('./cjs/react-media.js'); 7 | } 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | let mappedModule; 2 | switch (process.env.TEST_ENV) { 3 | case 'cjs': 4 | mappedModule = '/cjs/react-media.js'; 5 | break; 6 | case 'umd': 7 | mappedModule = '/umd/react-media.js'; 8 | break; 9 | case 'source': 10 | default: 11 | mappedModule = '/modules/index.js'; 12 | } 13 | 14 | module.exports = { 15 | globals: { 16 | __DEV__: true 17 | }, 18 | moduleNameMapper: { 19 | '^react-media$': mappedModule 20 | }, 21 | testMatch: ['**/__tests__/**/*-test.js'], 22 | testURL: 'http://localhost/' 23 | }; 24 | -------------------------------------------------------------------------------- /modules/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "loose": true 7 | } 8 | ], 9 | "@babel/react" 10 | ], 11 | "plugins": [ 12 | "@babel/plugin-proposal-export-default-from", 13 | "dev-expression" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /modules/Media.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import PropTypes from "prop-types"; 3 | import invariant from "invariant"; 4 | import json2mq from "json2mq"; 5 | 6 | import MediaQueryListener from "./MediaQueryListener"; 7 | 8 | const checkInvariants = ({ query, queries, defaultMatches }) => { 9 | invariant( 10 | !(!query && !queries) || (query && queries), 11 | ' must be supplied with either "query" or "queries"' 12 | ); 13 | 14 | invariant( 15 | defaultMatches === undefined || 16 | !query || 17 | typeof defaultMatches === "boolean", 18 | " when query is set, defaultMatches must be a boolean, received " + 19 | typeof defaultMatches 20 | ); 21 | 22 | invariant( 23 | defaultMatches === undefined || 24 | !queries || 25 | typeof defaultMatches === "object", 26 | " when queries is set, defaultMatches must be a object of booleans, received " + 27 | typeof defaultMatches 28 | ); 29 | } 30 | 31 | /** 32 | * Wraps a single query in an object. This is used to provide backward compatibility with 33 | * the old `query` prop (as opposed to `queries`). If only a single query is passed, the object 34 | * will be unpacked down the line, but this allows our internals to assume an object of queries 35 | * at all times. 36 | */ 37 | const wrapInQueryObject = query => ({ __DEFAULT__: query }); 38 | 39 | /** 40 | * Unwraps an object of queries, if it was originally passed as a single query. 41 | */ 42 | const unwrapSingleQuery = queryObject => { 43 | const queryNames = Object.keys(queryObject); 44 | if (queryNames.length === 1 && queryNames[0] === "__DEFAULT__") { 45 | return queryObject.__DEFAULT__; 46 | } 47 | return queryObject; 48 | }; 49 | 50 | export const useMedia = ({ query, queries, defaultMatches, targetWindow, onChange }) => { 51 | checkInvariants({ query, queries, defaultMatches }); 52 | const activeQueries = useRef([]); 53 | const getMatches = () => { 54 | const result = activeQueries.current.reduce( 55 | (acc, { name, mqListener }) => ({ ...acc, [name]: mqListener.matches }), 56 | {}, 57 | ); 58 | 59 | // return result; 60 | return unwrapSingleQuery(result); 61 | }; 62 | const updateMatches = () => { 63 | setMatches(getMatches()); 64 | }; 65 | 66 | const setUpMQLs = () => { 67 | const activeTargetWindow = targetWindow || window; 68 | 69 | invariant( 70 | typeof activeTargetWindow.matchMedia === "function", 71 | " does not support `matchMedia`." 72 | ); 73 | 74 | const queryObject = queries || wrapInQueryObject(query); 75 | 76 | activeQueries.current = Object.keys(queryObject).map(name => { 77 | const currentQuery = queryObject[name]; 78 | const qs = typeof currentQuery !== "string" ? json2mq(currentQuery) : currentQuery; 79 | const mqListener = new MediaQueryListener( 80 | activeTargetWindow, 81 | qs, 82 | updateMatches, 83 | ); 84 | 85 | return { name, mqListener }; 86 | }); 87 | }; 88 | 89 | const [matches, setMatches] = useState(() => { 90 | // If props.defaultMatches has been set, ensure we trigger a two-pass render. 91 | // This is useful for SSR with mismatching defaultMatches vs actual matches from window.matchMedia 92 | // Details: https://github.com/ReactTraining/react-media/issues/81 93 | // TODO: figure out whether this is still technically a two-pass render. 94 | if (typeof window !== "object") { 95 | // In case we're rendering on the server, apply the default matches 96 | if (defaultMatches !== undefined) { 97 | return defaultMatches; 98 | } 99 | if (query) { 100 | return true; 101 | } 102 | /* if (props.queries) */ 103 | return Object.keys(queries).reduce( 104 | (acc, key) => ({ ...acc, [key]: true }), 105 | {} 106 | ); 107 | } 108 | // Else we'll use the state from the MQLs that were just set up. 109 | setUpMQLs(); 110 | return getMatches(); 111 | }); 112 | 113 | 114 | useEffect( 115 | // Because setup happens in the state constructor, cleanup is the only thing that 116 | // useEffect is responsible for. 117 | // eslint-disable-next-line react-hooks/exhaustive-deps 118 | () => () => activeQueries.current.forEach(({ mqListener }) => mqListener.cancel()), 119 | [], 120 | ); 121 | 122 | useEffect( 123 | // Set up a separate listener for onChange since we ideally want to fire onChange 124 | // after flushes, rather than having to insert it synchronously before an update happens. 125 | () => { 126 | if (onChange) { 127 | onChange(matches); 128 | } 129 | }, 130 | [matches, onChange], 131 | ); 132 | return matches; 133 | }; 134 | 135 | /** 136 | * Conditionally renders based on whether or not a media query matches. 137 | */ 138 | const Media = ({ 139 | defaultMatches, 140 | query, 141 | queries, 142 | render, 143 | children, 144 | targetWindow, 145 | onChange, 146 | }) => { 147 | const matches = useMedia({ query, queries, defaultMatches, targetWindow, onChange }); 148 | 149 | // render 150 | const isAnyMatches = 151 | typeof matches === "object" 152 | ? Object.keys(matches).some(key => matches[key]) 153 | : matches; 154 | 155 | return ( 156 | render 157 | ? ( 158 | isAnyMatches 159 | ? render(matches) 160 | : null 161 | ) 162 | : ( 163 | children 164 | ? ( 165 | typeof children === "function" 166 | ? children(matches) 167 | : ( 168 | (!Array.isArray(children) || children.length) // Preact defaults to empty children array 169 | ? (isAnyMatches 170 | // We have to check whether child is a composite component or not to decide should we 171 | // provide `matches` as a prop or not 172 | ? ( 173 | (React.Children.only(children) 174 | && typeof React.Children.only(children).type === "string") 175 | ? React.Children.only(children) 176 | : React.cloneElement(React.Children.only(children), { matches }) 177 | ) 178 | : null 179 | ) 180 | : null 181 | ) 182 | ) 183 | : null 184 | ) 185 | ); 186 | } 187 | 188 | const queryType = PropTypes.oneOfType([ 189 | PropTypes.string, 190 | PropTypes.object, 191 | PropTypes.arrayOf(PropTypes.object.isRequired) 192 | ]); 193 | 194 | Media.propTypes = { 195 | defaultMatches: PropTypes.oneOfType([ 196 | PropTypes.bool, 197 | PropTypes.objectOf(PropTypes.bool) 198 | ]), 199 | query: queryType, 200 | queries: PropTypes.objectOf(queryType), 201 | render: PropTypes.func, 202 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), 203 | targetWindow: PropTypes.object, 204 | onChange: PropTypes.func 205 | }; 206 | 207 | export default Media; 208 | -------------------------------------------------------------------------------- /modules/MediaQueryListener.js: -------------------------------------------------------------------------------- 1 | export default class MediaQueryListener { 2 | constructor(targetWindow, query, listener) { 3 | this.nativeMediaQueryList = targetWindow.matchMedia(query); 4 | this.active = true; 5 | // Safari doesn't clear up listener with removeListener 6 | // when the listener is already waiting in the event queue. 7 | // Having an active flag to make sure the listener is not called 8 | // after we removeListener. 9 | this.cancellableListener = (...args) => { 10 | this.matches = this.nativeMediaQueryList.matches; 11 | if (this.active) { 12 | listener(...args); 13 | } 14 | }; 15 | this.nativeMediaQueryList.addListener(this.cancellableListener); 16 | this.matches = this.nativeMediaQueryList.matches; 17 | } 18 | 19 | cancel() { 20 | this.active = false; 21 | this.nativeMediaQueryList.removeListener(this.cancellableListener); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /modules/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "jest" 4 | ], 5 | "env": { 6 | "jest/globals": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/__tests__/Media-ssr-test.js: -------------------------------------------------------------------------------- 1 | /** @jest-environment node */ 2 | 3 | import React from "react"; 4 | import Media from "../Media"; 5 | 6 | import { serverRenderStrict } from "./utils"; 7 | 8 | describe("A in server environment", () => { 9 | describe("and a single query is defined", () => { 10 | const query = "(max-width: 1000px)"; 11 | 12 | describe("when no default matches prop provided", () => { 13 | it("should render its children as if the query matches", () => { 14 | const element = ( 15 | 16 | {matches => 17 | matches === true ? Matches, render! : null 18 | } 19 | 20 | ); 21 | 22 | const result = serverRenderStrict(element); 23 | 24 | expect(result).toBe("Matches, render!"); 25 | }); 26 | }); 27 | 28 | describe("when default matches prop provided", () => { 29 | it("should render its children according to the provided defaultMatches", () => { 30 | const render = matches => (matches === true ? matches : null); 31 | 32 | const matched = ( 33 | 34 | {render} 35 | 36 | ); 37 | 38 | const matchedResult = serverRenderStrict(matched); 39 | 40 | expect(matchedResult).toBe("matches"); 41 | 42 | const notMatched = ( 43 | 44 | {render} 45 | 46 | ); 47 | 48 | const notMatchedResult = serverRenderStrict(notMatched); 49 | 50 | expect(notMatchedResult).toBe(""); 51 | }); 52 | }); 53 | }); 54 | 55 | describe("and multiple queries are defined", () => { 56 | const queries = { 57 | sm: "(max-width: 1000px)", 58 | lg: "(max-width: 2000px)", 59 | xl: "(max-width: 3000px)" 60 | }; 61 | 62 | describe("when no default matches prop provided", () => { 63 | it("should render its children as if all queries are matching", () => { 64 | const element = ( 65 | 66 | {matches => 67 | matches.sm && matches.lg && matches.xl ? ( 68 | All matches, render! 69 | ) : null 70 | } 71 | 72 | ); 73 | 74 | const result = serverRenderStrict(element); 75 | 76 | expect(result).toBe("All matches, render!"); 77 | }); 78 | }); 79 | 80 | describe("when default matches prop provided", () => { 81 | const defaultMatches = { 82 | sm: true, 83 | lg: false, 84 | xl: false 85 | }; 86 | 87 | it("should render its children according to the provided defaultMatches", () => { 88 | const element = ( 89 | 90 | {matches => ( 91 |
92 | {matches.sm && small} 93 | {matches.lg && large} 94 | {matches.xl && extra large} 95 |
96 | )} 97 |
98 | ); 99 | 100 | const result = serverRenderStrict(element); 101 | 102 | expect(result).toBe("
small
"); 103 | }); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /modules/__tests__/Media-test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Media from "../Media"; 3 | import { renderStrict } from "./utils"; 4 | 5 | const createMockMediaMatcher = matchesOrMapOfMatches => qs => ({ 6 | matches: 7 | typeof matchesOrMapOfMatches === "object" 8 | ? matchesOrMapOfMatches[qs] 9 | : matchesOrMapOfMatches, 10 | addListener: () => {}, 11 | removeListener: () => {} 12 | }); 13 | 14 | describe("A in browser environment", () => { 15 | let originalMatchMedia; 16 | 17 | beforeEach(() => { 18 | originalMatchMedia = window.matchMedia; 19 | }); 20 | 21 | afterEach(() => { 22 | window.matchMedia = originalMatchMedia; 23 | }); 24 | 25 | const queries = { 26 | sm: "(max-width: 1000px)", 27 | lg: "(max-width: 2000px)" 28 | }; 29 | 30 | describe("with a query that matches", () => { 31 | beforeEach(() => { 32 | window.matchMedia = createMockMediaMatcher(true); 33 | }); 34 | 35 | describe("and a child DOM element", () => { 36 | it("should render child", () => { 37 | const { container } = renderStrict( 38 | 39 |
matched
40 |
41 | ); 42 | 43 | expect(container.innerHTML).toMatch("matched"); 44 | }); 45 | }); 46 | 47 | describe("and a child component", () => { 48 | it("should render child and provide matches as a prop", () => { 49 | const Component = props => 50 | // eslint-disable-next-line react/prop-types 51 | props.matches === true && matched; 52 | 53 | const { container } = renderStrict( 54 | 55 | 56 | 57 | ); 58 | 59 | expect(container.innerHTML).toMatch("matched"); 60 | }); 61 | }); 62 | 63 | describe("and a children function", () => { 64 | it("should render its children function call result", () => { 65 | const { container } = renderStrict( 66 | 67 | {matches => 68 | matches === true ? children as a function : null 69 | } 70 | 71 | ); 72 | 73 | expect(container.innerHTML).toMatch("children as a function"); 74 | }); 75 | }); 76 | 77 | describe("and a render prop", () => { 78 | it("should render `render` prop call result", () => { 79 | const { container } = renderStrict( 80 | matches === true && render prop} 83 | /> 84 | ); 85 | 86 | expect(container.innerHTML).toMatch("render prop"); 87 | }); 88 | }); 89 | }); 90 | 91 | describe("with multiple queries that match", () => { 92 | beforeEach(() => { 93 | window.matchMedia = createMockMediaMatcher(true); 94 | }); 95 | 96 | describe("and a child DOM element", () => { 97 | it("should render child", () => { 98 | const { container } = renderStrict( 99 | 100 |
fully matched
101 |
102 | ); 103 | 104 | expect(container.innerHTML).toMatch("fully matched"); 105 | }); 106 | }); 107 | 108 | describe("and a child component", () => { 109 | it("should render child and provide matches as a prop", () => { 110 | const Component = props => 111 | // eslint-disable-next-line react/prop-types 112 | props.matches.sm && props.matches.lg && fully matched; 113 | 114 | const { container } = renderStrict( 115 | 116 | 117 | 118 | ); 119 | 120 | expect(container.innerHTML).toMatch("fully matched"); 121 | }); 122 | }); 123 | 124 | describe("and a children function", () => { 125 | it("should render its children function call result", () => { 126 | const { container } = renderStrict( 127 | 128 | {matches => 129 | matches.sm && matches.lg ? ( 130 | children as a function 131 | ) : null 132 | } 133 | 134 | ); 135 | 136 | expect(container.innerHTML).toMatch("children as a function"); 137 | }); 138 | }); 139 | 140 | describe("and a render prop", () => { 141 | it("should render `render` prop call result", () => { 142 | const { container } = renderStrict( 143 | 146 | matches.sm && matches.lg && render prop 147 | } 148 | /> 149 | ); 150 | 151 | expect(container.innerHTML).toMatch("render prop"); 152 | }); 153 | }); 154 | }); 155 | 156 | describe("with a query that does not match", () => { 157 | beforeEach(() => { 158 | window.matchMedia = createMockMediaMatcher(false); 159 | }); 160 | 161 | describe("and a child DOM element", () => { 162 | it("should not render anything", () => { 163 | const { container } = ( 164 | 165 |
I am not rendered
166 |
167 | ); 168 | 169 | expect(container).toBeUndefined(); 170 | }); 171 | }); 172 | 173 | describe("and a child component", () => { 174 | it("should not render anything", () => { 175 | const Component = () => I am not rendered; 176 | 177 | const { container } = ( 178 | 179 | 180 | 181 | ); 182 | 183 | expect(container).toBeUndefined(); 184 | }); 185 | }); 186 | 187 | describe("and a children function", () => { 188 | it("should render children function call result", () => { 189 | const { container } = renderStrict( 190 | 191 | {matches => matches === false && no matches at all} 192 | 193 | ); 194 | 195 | expect(container.innerHTML).toMatch("no matches at all"); 196 | }); 197 | }); 198 | 199 | describe("and a render prop", () => { 200 | it("should not call render prop at all", () => { 201 | const render = jest.fn(); 202 | 203 | renderStrict(); 204 | 205 | expect(render).not.toBeCalled(); 206 | }); 207 | }); 208 | }); 209 | 210 | describe("with a multiple queries that do not match", () => { 211 | beforeEach(() => { 212 | window.matchMedia = createMockMediaMatcher(false); 213 | }); 214 | 215 | describe("and a child DOM element", () => { 216 | it("should not render anything", () => { 217 | const { container } = renderStrict( 218 | 219 |
I am not rendered
220 |
221 | ); 222 | 223 | expect(container.innerHTML || "").not.toMatch("I am not rendered"); 224 | }); 225 | }); 226 | 227 | describe("and a child component", () => { 228 | it("should not render anything", () => { 229 | const Component = () => I am not rendered; 230 | 231 | const { container } = renderStrict( 232 | 233 | 234 | 235 | ); 236 | 237 | expect(container.innerHTML || "").not.toMatch("I am not rendered"); 238 | }); 239 | }); 240 | 241 | describe("and a children function", () => { 242 | it("should render children function call result", () => { 243 | const { container } = renderStrict( 244 | 245 | {matches => 246 | !matches.sm && !matches.lg && no matches at all 247 | } 248 | 249 | ); 250 | 251 | expect(container.innerHTML).toMatch("no matches at all"); 252 | }); 253 | }); 254 | 255 | describe("and a render prop", () => { 256 | it("should not call render prop at all", () => { 257 | const render = jest.fn(); 258 | 259 | renderStrict(); 260 | 261 | expect(render).not.toBeCalled(); 262 | }); 263 | }); 264 | }); 265 | 266 | describe("with queries that partially match", () => { 267 | const queries = { 268 | sm: "(max-width: 1000px)", 269 | lg: "(max-width: 2000px)" 270 | }; 271 | 272 | const matches = { 273 | "(max-width: 1000px)": true, 274 | "(max-width: 2000px)": false 275 | }; 276 | 277 | beforeEach(() => { 278 | window.matchMedia = createMockMediaMatcher(matches); 279 | }); 280 | 281 | describe("and a child component", () => { 282 | it("should render child and provide matches as a prop", () => { 283 | /* eslint-disable react/prop-types */ 284 | const Component = props => 285 | props.matches.sm && 286 | !props.matches.lg && partially matched; 287 | /* eslint-enable */ 288 | 289 | const { container } = renderStrict( 290 | 291 | 292 | 293 | ); 294 | 295 | expect(container.innerHTML).toMatch("partially matched"); 296 | }); 297 | }); 298 | 299 | describe("and a children function", () => { 300 | it("should render children function call result", () => { 301 | const { container } = renderStrict( 302 | 303 | {matches => 304 | matches.sm && 305 | !matches.lg && yep, something definitely matched 306 | } 307 | 308 | ); 309 | 310 | expect(container.innerHTML).toMatch("yep, something definitely matched"); 311 | }); 312 | }); 313 | 314 | describe("and a render prop", () => { 315 | it("should render `render` prop call result", () => { 316 | const { container } = renderStrict( 317 | 320 | matches.sm && !matches.lg && please render me 321 | } 322 | /> 323 | ); 324 | 325 | expect(container.innerHTML).toMatch("please render me"); 326 | }); 327 | }); 328 | }); 329 | 330 | describe("when a custom targetWindow prop is passed", () => { 331 | const queries = { matches: { maxWidth: 320 } }; 332 | 333 | beforeEach(() => { 334 | window.matchMedia = createMockMediaMatcher(true); 335 | }); 336 | 337 | it("renders its child", () => { 338 | const testWindow = { 339 | matchMedia: createMockMediaMatcher(false) 340 | }; 341 | 342 | const { container } = renderStrict( 343 | 344 | {({ matches }) => (matches ?
hello
:
goodbye
)} 345 |
346 | ); 347 | 348 | expect(container.innerHTML).toMatch(/goodbye/); 349 | }); 350 | 351 | describe("when a non-window prop is passed for targetWindow", () => { 352 | it("errors with a useful message", () => { 353 | const notAWindow = {}; 354 | 355 | const element = ( 356 | 357 | {({ matches }) => (matches ?
hello
:
goodbye
)} 358 |
359 | ); 360 | 361 | jest.spyOn(console, 'error') 362 | /* eslint-disable no-console */ 363 | // This is to prevent stderr output from jsdom from showing up 364 | // in the test output for this particular test. 365 | console.error.mockImplementation(() => {}) 366 | 367 | expect(() => renderStrict(element)).toThrow("does not support `matchMedia`"); 368 | console.error.mockRestore() 369 | /* eslint-enable */ 370 | }); 371 | }); 372 | }); 373 | 374 | describe("when an onChange function is passed", () => { 375 | beforeEach(() => { 376 | window.matchMedia = createMockMediaMatcher(true); 377 | }); 378 | 379 | it("calls the function with the match result", () => { 380 | const callback = jest.fn(); 381 | renderStrict(); 382 | 383 | expect(callback).toHaveBeenCalledWith({ matches: true }); 384 | }); 385 | }); 386 | 387 | describe("when defaultMatches have been passed", () => { 388 | beforeEach(() => { 389 | window.matchMedia = createMockMediaMatcher(false); 390 | }); 391 | 392 | describe("for a single query", () => { 393 | it("initially overwrites defaultMatches with matches from matchMedia", async () => { 394 | const { container } = renderStrict( 395 | 396 | {matches => 397 | matches === true ? ( 398 |
fully matched
399 | ) : ( 400 |
not matched
401 | ) 402 | } 403 |
404 | ); 405 | 406 | expect(container.innerHTML).toMatch("not matched"); 407 | }); 408 | }); 409 | 410 | describe("for multiple queries", () => { 411 | it("initially overwrites defaultMatches with matches from matchMedia", async () => { 412 | const { container } = renderStrict( 413 | 414 | {({ matches }) => 415 | matches ?
fully matched
:
not matched
416 | } 417 |
418 | ); 419 | 420 | expect(container.innerHTML).toMatch("not matched"); 421 | }); 422 | }); 423 | }); 424 | }); 425 | -------------------------------------------------------------------------------- /modules/__tests__/utils.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from '@testing-library/react'; 3 | import ReactDOMServer from 'react-dom/server'; 4 | 5 | let StrictMode = function(props) { 6 | return props.children || null; 7 | }; 8 | 9 | if (React.StrictMode) { 10 | StrictMode = React.StrictMode; 11 | } 12 | 13 | export function renderStrict(element) { 14 | return render({element}); 15 | } 16 | 17 | export function serverRenderStrict(element) { 18 | return ReactDOMServer.renderToStaticMarkup({element}); 19 | } 20 | -------------------------------------------------------------------------------- /modules/index.js: -------------------------------------------------------------------------------- 1 | export default, { useMedia } from './Media'; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-media", 3 | "version": "2.0.0-rc.1", 4 | "description": "CSS media queries for React", 5 | "repository": "ReactTraining/react-media", 6 | "license": "MIT", 7 | "author": "Michael Jackson", 8 | "files": [ 9 | "cjs", 10 | "esm", 11 | "umd", 12 | "index.d.ts" 13 | ], 14 | "main": "index.js", 15 | "module": "esm/react-media.js", 16 | "unpkg": "umd/react-media.js", 17 | "types": "index.d.ts", 18 | "scripts": { 19 | "build": "rollup -c", 20 | "prepublishOnly": "npm run build", 21 | "clean": "git clean -fdX .", 22 | "lint": "eslint modules", 23 | "test": "jest", 24 | "testTypes": "tsc --noEmit --jsx react typeTest.tsx" 25 | }, 26 | "peerDependencies": { 27 | "react": "^16.8.0" 28 | }, 29 | "dependencies": { 30 | "@babel/runtime": "^7.9.6", 31 | "invariant": "^2.2.2", 32 | "json2mq": "^0.2.0", 33 | "prop-types": "^15.5.10" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.9.6", 37 | "@babel/plugin-proposal-export-default-from": "^7.8.3", 38 | "@babel/plugin-transform-runtime": "^7.9.6", 39 | "@babel/preset-env": "^7.9.6", 40 | "@babel/preset-react": "^7.9.4", 41 | "@rollup/plugin-babel": "5.0.0", 42 | "@rollup/plugin-commonjs": "^11.1.0", 43 | "@rollup/plugin-node-resolve": "^7.1.3", 44 | "@rollup/plugin-replace": "^2.3.2", 45 | "@testing-library/jest-dom": "^5.7.0", 46 | "@testing-library/react": "^10.0.4", 47 | "@types/react": "^16.8.18", 48 | "babel-core": "^7.0.0-bridge.0", 49 | "babel-eslint": "^10.0.1", 50 | "babel-jest": "^23.4.2", 51 | "babel-plugin-dev-expression": "^0.2.2", 52 | "babel-plugin-transform-react-remove-prop-types": "^0.4.12", 53 | "eslint": "^5.9.0", 54 | "eslint-plugin-import": "^2.14.0", 55 | "eslint-plugin-jest": "^22.1.0", 56 | "eslint-plugin-react": "^7.11.1", 57 | "eslint-plugin-react-hooks": "^4.0.0", 58 | "gzip-size": "^3.0.0", 59 | "jest": "^23.6.0", 60 | "pascal-case": "^2.0.1", 61 | "pretty-bytes": "^4.0.2", 62 | "react": "^16.8.0", 63 | "react-dom": "^16.8.0", 64 | "rollup": "^2.9.0", 65 | "rollup-plugin-size-snapshot": "^0.11.0", 66 | "rollup-plugin-terser": "^5.3.0", 67 | "typescript": "^3.4.5" 68 | }, 69 | "keywords": [ 70 | "react", 71 | "media", 72 | "media query", 73 | "query", 74 | "css", 75 | "responsive" 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import replace from '@rollup/plugin-replace'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import nodeResolve from '@rollup/plugin-node-resolve'; 5 | import { sizeSnapshot } from 'rollup-plugin-size-snapshot'; 6 | import { terser } from 'rollup-plugin-terser'; 7 | 8 | import pkg from './package.json'; 9 | 10 | const input = 'modules/index.js'; 11 | const globalName = 'ReactMedia'; 12 | const globals = { react: 'React' }; 13 | 14 | function external(id) { 15 | return !id.startsWith('.') && !id.startsWith('/'); 16 | } 17 | 18 | const cjs = [ 19 | { 20 | input, 21 | output: { file: `cjs/${pkg.name}.js`, format: 'cjs', exports: 'named' }, 22 | external, 23 | plugins: [ 24 | babel({ exclude: /node_modules/, babelHelpers: 'bundled' }), 25 | replace({ 'process.env.NODE_ENV': JSON.stringify('development') }) 26 | ] 27 | }, 28 | 29 | { 30 | input, 31 | output: { file: `cjs/${pkg.name}.min.js`, format: 'cjs', exports: 'named' }, 32 | external, 33 | plugins: [ 34 | babel({ exclude: /node_modules/, babelHelpers: 'bundled' }), 35 | replace({ 'process.env.NODE_ENV': JSON.stringify('production') }), 36 | terser() 37 | ] 38 | } 39 | ]; 40 | 41 | const esm = [ 42 | { 43 | input, 44 | output: { file: `esm/${pkg.name}.js`, format: 'esm', exports: 'named' }, 45 | external, 46 | plugins: [ 47 | babel({ 48 | exclude: /node_modules/, 49 | babelHelpers: 'runtime', 50 | plugins: [['@babel/transform-runtime', { useESModules: true }]] 51 | }), 52 | sizeSnapshot() 53 | ] 54 | } 55 | ]; 56 | 57 | const umd = [ 58 | { 59 | input, 60 | output: { 61 | file: `umd/${pkg.name}.js`, 62 | format: 'umd', 63 | name: globalName, 64 | exports: 'named', 65 | globals 66 | }, 67 | external: Object.keys(globals), 68 | plugins: [ 69 | babel({ 70 | exclude: /node_modules/, 71 | babelHelpers: 'runtime', 72 | plugins: [['@babel/transform-runtime', { useESModules: true }]] 73 | }), 74 | nodeResolve(), 75 | commonjs({ 76 | include: /node_modules/ 77 | }), 78 | replace({ 'process.env.NODE_ENV': JSON.stringify('development') }), 79 | sizeSnapshot() 80 | ] 81 | }, 82 | 83 | { 84 | input, 85 | output: { 86 | file: `umd/${pkg.name}.min.js`, 87 | format: 'umd', 88 | name: globalName, 89 | exports: 'named', 90 | globals 91 | }, 92 | external: Object.keys(globals), 93 | plugins: [ 94 | babel({ 95 | exclude: /node_modules/, 96 | babelHelpers: 'runtime', 97 | plugins: [['@babel/transform-runtime', { useESModules: true }]] 98 | }), 99 | nodeResolve(), 100 | commonjs({ 101 | include: /node_modules/ 102 | }), 103 | replace({ 'process.env.NODE_ENV': JSON.stringify('production') }), 104 | sizeSnapshot(), 105 | terser() 106 | ] 107 | } 108 | ]; 109 | 110 | let config; 111 | switch (process.env.BUILD_ENV) { 112 | case 'cjs': 113 | config = cjs; 114 | break; 115 | case 'esm': 116 | config = esm; 117 | break; 118 | case 'umd': 119 | config = umd; 120 | break; 121 | default: 122 | config = cjs.concat(esm).concat(umd); 123 | } 124 | 125 | export default config; 126 | -------------------------------------------------------------------------------- /typeTest.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Media from "."; 3 | 4 | /** 5 | * This file is used to test the types in `index.d.ts`. 6 | */ 7 | 8 | export function SingleQuery() { 9 | return ( 10 | 11 | {matches => matches &&
it works
} 12 |
13 | ); 14 | } 15 | 16 | export function MultiQuery() { 17 | return ( 18 | console.log(matches.small)} 28 | > 29 | {matches => matches &&
it works
} 30 |
31 | ); 32 | } 33 | --------------------------------------------------------------------------------