From e56a1985f6a93b9e881bf639df54cda3105d48d7 Mon Sep 17 00:00:00 2001 From: Edo Rivai Date: Mon, 3 Sep 2018 12:14:38 +0200 Subject: [PATCH] Implement two-pass render (closes #81) --- modules/Media.js | 73 ++++++++++++++++++++++++--------- modules/__tests__/Media-test.js | 16 ++++++++ package.json | 2 +- 3 files changed, 71 insertions(+), 20 deletions(-) diff --git a/modules/Media.js b/modules/Media.js index 049fb7d..9dcf443 100644 --- a/modules/Media.js +++ b/modules/Media.js @@ -24,31 +24,51 @@ class Media extends React.Component { queries = []; - state = { - matches: - this.props.defaultMatches || - Object.keys(this.props.queries).reduce( - (acc, key) => ({ ...acc, [key]: true }), - {} - ) - }; + constructor(props) { + super(props); + + if (typeof window !== "object") { + // In case we're rendering on the server + this.state = { + matches: + this.props.defaultMatches || + Object.keys(this.props.queries).reduce( + (acc, key) => ({ ...acc, [key]: true }), + {} + ) + }; + return; + } - updateMatches = () => { - const newMatches = this.queries.reduce( + this.initialize(); + + // Instead of calling this.updateMatches, we manually set the state to prevent + // calling setState, which could trigger an unnecessary second render + this.state = { + matches: + this.props.defaultMatches !== undefined + ? this.props.defaultMatches + : this.getMatches() + }; + this.onChange(); + } + + getMatches = () => { + return this.queries.reduce( (acc, { name, mqList }) => ({ ...acc, [name]: mqList.matches }), {} ); - this.setState({ matches: newMatches }); - - const { onChange } = this.props; - if (onChange) { - onChange(newMatches); - } }; - componentWillMount() { - if (typeof window !== "object") return; + updateMatches = () => { + const newMatches = this.getMatches(); + this.setState(() => ({ + matches: newMatches + }), this.onChange); + }; + + initialize() { const targetWindow = this.props.targetWindow || window; invariant( @@ -67,8 +87,23 @@ class Media extends React.Component { return { name, qs, mqList }; }); + } - this.updateMatches(); + componentDidMount() { + this.initialize(); + // If props.defaultMatches has been set, ensure we trigger a two-pass render. + // This is useful for SSR with mismatching defaultMatches vs actual matches from window.matchMedia + // Details: https://github.com/ReactTraining/react-media/issues/81 + if (this.props.defaultMatches !== undefined) { + this.updateMatches(); + } + } + + onChange() { + const { onChange } = this.props; + if (onChange) { + onChange(this.state.matches); + } } componentWillUnmount() { diff --git a/modules/__tests__/Media-test.js b/modules/__tests__/Media-test.js index 74b8e44..fe7723b 100644 --- a/modules/__tests__/Media-test.js +++ b/modules/__tests__/Media-test.js @@ -291,4 +291,20 @@ describe("A in browser environment", () => { }); }); }); + + describe("when defaultMatches have been passed", () => { + beforeEach(() => { + window.matchMedia = createMockMediaMatcher(false); + }); + + it("initially overwrites defaultMatches with matches from matchMedia", async () => { + const element = + {({ matches }) => matches ?
fully matched
:
not matched
} +
; + + ReactDOM.render(element, node, () => { + expect(node.firstChild.innerHTML).toMatch('not matched'); + }); + }); + }) }); diff --git a/package.json b/package.json index bc7ec05..edaff74 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "eslint-plugin-jest": "^20.0.3", "eslint-plugin-react": "^6.0.0", "gzip-size": "^3.0.0", - "jest": "^20.0.4", + "jest": "^23.5.0", "pascal-case": "^2.0.1", "pretty-bytes": "^4.0.2", "react": "^15.4.1 || ^0.14.7",