From 65d74e0f761f3159777729b77888e9c41278e2aa Mon Sep 17 00:00:00 2001 From: Matias Olivera Date: Tue, 31 Jul 2018 12:10:09 -0300 Subject: [PATCH 1/4] Add onQueryStateChange callback --- modules/Media.js | 14 +++++++++++++- modules/__tests__/Media-test.js | 34 ++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/modules/Media.js b/modules/Media.js index da71686..62da623 100644 --- a/modules/Media.js +++ b/modules/Media.js @@ -16,7 +16,8 @@ class Media extends React.Component { ]).isRequired, render: PropTypes.func, children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - targetWindow: PropTypes.object + targetWindow: PropTypes.object, + onQueryStateChange: PropTypes.func }; static defaultProps = { @@ -43,12 +44,23 @@ class Media extends React.Component { if (typeof query !== "string") query = json2mq(query); this.mediaQueryList = targetWindow.matchMedia(query); + + const { onQueryStateChange } = this.props; + if (onQueryStateChange) { + this.mediaQueryList.addListener(onQueryStateChange); + } + this.mediaQueryList.addListener(this.updateMatches); this.updateMatches(); } componentWillUnmount() { this.mediaQueryList.removeListener(this.updateMatches); + + const { onQueryStateChange } = this.props; + if (onQueryStateChange) { + this.mediaQueryList.removeListener(onQueryStateChange); + } } render() { diff --git a/modules/__tests__/Media-test.js b/modules/__tests__/Media-test.js index 49678d6..4e08667 100644 --- a/modules/__tests__/Media-test.js +++ b/modules/__tests__/Media-test.js @@ -3,10 +3,14 @@ import ReactDOM from "react-dom"; import ReactDOMServer from "react-dom/server"; import Media from "../Media"; -const createMockMediaMatcher = matches => () => ({ +const createMockMediaMatcher = ( matches, - addListener: () => {}, - removeListener: () => {} + addListener = () => {}, + removeListener = () => {} +) => () => ({ + matches, + addListener, + removeListener }); describe("A ", () => { @@ -160,6 +164,30 @@ describe("A ", () => { }); }); + describe("when an onQueryStateChange function is passed", () => { + const mockAddListener = jest.fn(); + beforeEach(() => { + window.matchMedia = createMockMediaMatcher(true, mockAddListener); + }); + + afterEach(() => { + mockAddListener.mockClear(); + }); + + it("adds the function as a listener to the media query", () => { + const callback = () => {}; + const element = ( + + {matches => (matches ?
hello
:
goodbye
)} +
+ ); + + ReactDOM.render(element, node, () => { + expect(mockAddListener).toHaveBeenCalledWith(callback); + }); + }); + }); + describe("rendered on the server", () => { beforeEach(() => { window.matchMedia = createMockMediaMatcher(true); From 576955ef9494fc9c80fea099a730ee732164575a Mon Sep 17 00:00:00 2001 From: Matias Olivera Date: Sun, 26 Aug 2018 16:18:31 -0300 Subject: [PATCH 2/4] Rename callback to onChange --- modules/Media.js | 22 +++++++++++++--------- modules/__tests__/Media-test.js | 4 ++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/modules/Media.js b/modules/Media.js index 62da623..1519679 100644 --- a/modules/Media.js +++ b/modules/Media.js @@ -17,7 +17,7 @@ class Media extends React.Component { render: PropTypes.func, children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), targetWindow: PropTypes.object, - onQueryStateChange: PropTypes.func + onChange: PropTypes.func }; static defaultProps = { @@ -45,9 +45,9 @@ class Media extends React.Component { this.mediaQueryList = targetWindow.matchMedia(query); - const { onQueryStateChange } = this.props; - if (onQueryStateChange) { - this.mediaQueryList.addListener(onQueryStateChange); + const { onChange } = this.props; + if (onChange) { + this.mediaQueryList.addListener(onChange); } this.mediaQueryList.addListener(this.updateMatches); @@ -57,9 +57,9 @@ class Media extends React.Component { componentWillUnmount() { this.mediaQueryList.removeListener(this.updateMatches); - const { onQueryStateChange } = this.props; - if (onQueryStateChange) { - this.mediaQueryList.removeListener(onQueryStateChange); + const { onChange } = this.props; + if (onChange) { + this.mediaQueryList.removeListener(onChange); } } @@ -68,12 +68,16 @@ class Media extends React.Component { const { matches } = this.state; return render - ? matches ? render() : null + ? matches + ? render() + : null : children ? typeof children === "function" ? children(matches) : !Array.isArray(children) || children.length // Preact defaults to empty children array - ? matches ? React.Children.only(children) : null + ? matches + ? React.Children.only(children) + : null : null : null; } diff --git a/modules/__tests__/Media-test.js b/modules/__tests__/Media-test.js index 4e08667..285b48d 100644 --- a/modules/__tests__/Media-test.js +++ b/modules/__tests__/Media-test.js @@ -164,7 +164,7 @@ describe("A ", () => { }); }); - describe("when an onQueryStateChange function is passed", () => { + describe("when an onChange function is passed", () => { const mockAddListener = jest.fn(); beforeEach(() => { window.matchMedia = createMockMediaMatcher(true, mockAddListener); @@ -177,7 +177,7 @@ describe("A ", () => { it("adds the function as a listener to the media query", () => { const callback = () => {}; const element = ( - + {matches => (matches ?
hello
:
goodbye
)}
); From 7802ee9a2c4f2adff238207820de3e5fb6232ef6 Mon Sep 17 00:00:00 2001 From: Matias Olivera Date: Mon, 27 Aug 2018 14:18:28 -0300 Subject: [PATCH 3/4] Reuse `updateMatches` listener --- modules/Media.js | 22 ++++++++++------------ modules/__tests__/Media-test.js | 29 ++++++++--------------------- 2 files changed, 18 insertions(+), 33 deletions(-) diff --git a/modules/Media.js b/modules/Media.js index 1519679..1ca8ded 100644 --- a/modules/Media.js +++ b/modules/Media.js @@ -28,7 +28,16 @@ class Media extends React.Component { matches: this.props.defaultMatches }; - updateMatches = () => this.setState({ matches: this.mediaQueryList.matches }); + updateMatches = () => { + const { matches } = this.mediaQueryList; + + this.setState({ matches }); + + const { onChange } = this.props; + if (onChange) { + onChange(matches); + } + }; componentWillMount() { if (typeof window !== "object") return; @@ -44,23 +53,12 @@ class Media extends React.Component { if (typeof query !== "string") query = json2mq(query); this.mediaQueryList = targetWindow.matchMedia(query); - - const { onChange } = this.props; - if (onChange) { - this.mediaQueryList.addListener(onChange); - } - this.mediaQueryList.addListener(this.updateMatches); this.updateMatches(); } componentWillUnmount() { this.mediaQueryList.removeListener(this.updateMatches); - - const { onChange } = this.props; - if (onChange) { - this.mediaQueryList.removeListener(onChange); - } } render() { diff --git a/modules/__tests__/Media-test.js b/modules/__tests__/Media-test.js index 285b48d..0ab65e2 100644 --- a/modules/__tests__/Media-test.js +++ b/modules/__tests__/Media-test.js @@ -3,14 +3,10 @@ import ReactDOM from "react-dom"; import ReactDOMServer from "react-dom/server"; import Media from "../Media"; -const createMockMediaMatcher = ( +const createMockMediaMatcher = matches => () => ({ matches, - addListener = () => {}, - removeListener = () => {} -) => () => ({ - matches, - addListener, - removeListener + addListener: () => {}, + removeListener: () => {} }); describe("A ", () => { @@ -165,25 +161,16 @@ describe("A ", () => { }); describe("when an onChange function is passed", () => { - const mockAddListener = jest.fn(); beforeEach(() => { - window.matchMedia = createMockMediaMatcher(true, mockAddListener); - }); - - afterEach(() => { - mockAddListener.mockClear(); + window.matchMedia = createMockMediaMatcher(true); }); - it("adds the function as a listener to the media query", () => { - const callback = () => {}; - const element = ( - - {matches => (matches ?
hello
:
goodbye
)} -
- ); + it("calls the function with the match result", () => { + const callback = jest.fn(); + const element = ; ReactDOM.render(element, node, () => { - expect(mockAddListener).toHaveBeenCalledWith(callback); + expect(callback).toHaveBeenCalledWith(true); }); }); }); From 8b5e738380bd06a3d9c5a9ff2611b2bd3378dddd Mon Sep 17 00:00:00 2001 From: Matias Olivera Date: Mon, 27 Aug 2018 14:32:46 -0300 Subject: [PATCH 4/4] Add `onChange` prop documentation to README --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 90090fa..69c5cb4 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,30 @@ Keys of media query objects are camel-cased and numeric values automatically get An optional `targetWindow` prop can be specified if you want the `query` to be evaluated against a different window object than the one the code is running in. This can be useful for example 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). +There is also an optional `onChange` prop, which is a callback function that will be invoked when the status of the media query changes. This can be useful if you need to do some imperative stuff in addition to the declarative approach `react-media` already provides. + +```jsx +import React from "react"; +import Media from "react-media"; + +class App extends React.Component { + render() { + return ( +
+ + matches + ? alert("The document is less than 600px wide.") + : alert("The document is at least 600px wide.") + } + /> +
+ ); + } +} +``` + If you're curious about how react-media differs from [react-responsive](https://github.com/contra/react-responsive), please see [this comment](https://github.com/ReactTraining/react-media/issues/70#issuecomment-347774260). Enjoy!