Skip to content

Commit a9f7bfd

Browse files
committed
make React 16 hyrdrate compatible using two-pass rendering / rm matchmedia
1 parent 6894f14 commit a9f7bfd

14 files changed

+1536
-1429
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist

.eslintrc.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = {
2+
extends: 'airbnb',
3+
rules: {
4+
'react/jsx-filename-extension': 'off',
5+
},
6+
env: {
7+
browser: true,
8+
},
9+
};

README.md

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
# react-media-query-hoc
2-
A dead simple React Higher Order Component (HOC) that uses context for matching media queries
2+
A dead simple React Higher Order Component (HOC) that uses context for matching media queries.
33

44

55
## Why use this?
66
- A simple API which doesnt require you to put `MediaQuery` components all over your code base
77
- More performant (you only need 1 parent `MediaQueryProvider` that listens to media events you wish to configure)
88
- Easier to test than other react media query libraries
9-
- Uses [matchmedia](https://github.com/iceddev/matchmedia) for media queries for client and server
10-
- Abstracted away React context which is experimental (and subject to change for <= React 15)
9+
- Uses [css-mediaquery](https://github.com/ericf/css-mediaquery) for server side rendering
1110

1211
## Why not use this?
1312
We always recommend using vanilla CSS media queries to build responsive websites, this is simpler and provides a smoother UX, also it mitigates having to guess the screen width during [server side rendering](#server-side-rendering). At Domain we needed to use this component for legacy ad tech and advise against it's use for general responsive website design.
@@ -31,7 +30,7 @@ This library is designed so that you have 1 `MediaQueryProvider` parent and 1-ma
3130

3231
### `MediaQueryProvider`
3332

34-
This component will listen to media events you want to configure, it should be used once as a parent component
33+
This component will listen to media events you want to configure, it should be used once as a parent component.
3534

3635
**Usage:**
3736

@@ -71,7 +70,6 @@ const App = (props) => {
7170
### `withMedia`
7271

7372
This is a HOC to provide media match props to your component.
74-
This abstracts away context so that if there is any changes to the API in the future its easier to upgrade (see: [React Context](https://facebook.github.io/react/docs/context.html))
7573

7674
**Usage:**
7775
```javascript

dist/media-query-provider.js

+95-41
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@ Object.defineProperty(exports, "__esModule", {
44
value: true
55
});
66

7+
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
8+
79
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
810

911
var _react = require('react');
1012

1113
var _react2 = _interopRequireDefault(_react);
1214

13-
var _matchmedia = require('matchmedia');
14-
15-
var _matchmedia2 = _interopRequireDefault(_matchmedia);
16-
1715
var _propTypes = require('prop-types');
1816

1917
var _propTypes2 = _interopRequireDefault(_propTypes);
@@ -22,20 +20,21 @@ var _shallowequal = require('shallowequal');
2220

2321
var _shallowequal2 = _interopRequireDefault(_shallowequal);
2422

23+
var _cssMediaquery = require('css-mediaquery');
24+
25+
var _cssMediaquery2 = _interopRequireDefault(_cssMediaquery);
26+
2527
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
2628

29+
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
30+
2731
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
2832

2933
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
3034

3135
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
3236

33-
var defaultQueries = {
34-
mobile: 'screen and (max-width: 623px)',
35-
tablet: 'screen and (min-width: 624px) and (max-width: 1020px)',
36-
desktop: 'screen and (min-width: 1021px) and (max-width: 1440px)',
37-
largeDesktop: 'screen and (min-width: 1441px)'
38-
};
37+
var isServer = typeof process !== 'undefined';
3938

4039
var MediaQueryProvider = function (_React$Component) {
4140
_inherits(MediaQueryProvider, _React$Component);
@@ -45,28 +44,49 @@ var MediaQueryProvider = function (_React$Component) {
4544

4645
var _this = _possibleConstructorReturn(this, (MediaQueryProvider.__proto__ || Object.getPrototypeOf(MediaQueryProvider)).call(this, props));
4746

48-
_this.queryMedia = function (queries, values) {
49-
return Object.keys(queries).reduce(function (result, key) {
50-
var _matchMedia = (0, _matchmedia2.default)(queries[key], values),
51-
matches = _matchMedia.matches;
47+
var media = void 0;
5248

53-
result[key] = matches; // eslint-disable-line no-param-reassign
54-
return result;
49+
if (_this.props.values) {
50+
media = Object.entries(_this.props.queries).reduce(function (acc, _ref) {
51+
var _ref2 = _slicedToArray(_ref, 2),
52+
queryName = _ref2[0],
53+
query = _ref2[1];
54+
55+
// see README.md's "React 16 ReactDOM.hydrate" for info on why we use staticMatch
56+
// even in the browser
57+
acc[queryName] = _cssMediaquery2.default.match(query, _this.props.values);
58+
return acc;
5559
}, {});
56-
};
60+
} else {
61+
media = Object.entries(_this.props.queries).reduce(function (acc, _ref3) {
62+
var _ref4 = _slicedToArray(_ref3, 2),
63+
queryName = _ref4[0],
64+
query = _ref4[1];
65+
66+
// if the user has not set `values` and is server rendering, default to false
67+
// because we don't know the screen size
68+
acc[queryName] = isServer ? false : window.matchMedia(query).matches;
69+
return acc;
70+
}, {});
71+
}
5772

58-
_this.clientMatch = function () {
59-
var media = _this.queryMedia(_this.props.queries, {});
73+
// this is for the mediaQueryListener to be able to find the queryName from it's event arg
74+
_this.reverseQueries = Object.entries(_this.props.queries).reduce(function (acc, _ref5) {
75+
var _ref6 = _slicedToArray(_ref5, 2),
76+
queryName = _ref6[0],
77+
query = _ref6[1];
6078

61-
// no need to set state when it hasnt changed
62-
if (!(0, _shallowequal2.default)(media, _this.state.media)) {
63-
_this.setState({ media: media });
64-
}
65-
};
79+
acc[query] = queryName;
80+
return acc;
81+
}, {});
82+
83+
_this.mediaQueryListInstanceArray = [];
6684

6785
_this.state = {
68-
media: _this.queryMedia(props.queries, props.values)
86+
media: media
6987
};
88+
89+
_this.mediaQueryListener = _this.mediaQueryListener.bind(_this);
7090
return _this;
7191
}
7292

@@ -80,34 +100,63 @@ var MediaQueryProvider = function (_React$Component) {
80100
value: function componentDidMount() {
81101
var _this2 = this;
82102

83-
// even if we supplied values for SSR, they may not have matched up with client screen
84-
// so need to requery with client browser values
85-
this.clientMatch();
103+
var media = Object.entries(this.props.queries).reduce(function (acc, _ref7) {
104+
var _ref8 = _slicedToArray(_ref7, 2),
105+
queryName = _ref8[0],
106+
query = _ref8[1];
86107

87-
Object.keys(this.props.queries).forEach(function (key) {
88-
(0, _matchmedia2.default)(_this2.props.queries[key], _this2.props.values).addListener(_this2.clientMatch);
89-
});
108+
var mediaQueryListInstance = window.matchMedia(query);
109+
110+
mediaQueryListInstance.addListener(_this2.mediaQueryListener);
111+
112+
_this2.mediaQueryListInstanceArray.push(mediaQueryListInstance);
113+
114+
acc[queryName] = mediaQueryListInstance.matches;
115+
return acc;
116+
}, {});
117+
118+
if (!(0, _shallowequal2.default)(media, this.state.media)) {
119+
this.setState({ media: media }); // eslint-disable-line react/no-did-mount-set-state
120+
}
90121
}
91122
}, {
92123
key: 'componentWillUnmount',
93124
value: function componentWillUnmount() {
94125
var _this3 = this;
95126

96-
Object.keys(this.props.queries).forEach(function (key) {
97-
(0, _matchmedia2.default)(_this3.props.queries[key], _this3.props.values).removeListener(_this3.clientMatch);
127+
this.mediaQueryListInstanceArray.forEach(function (mediaQueryListInstance) {
128+
mediaQueryListInstance.removeListener(_this3.mediaQueryListener);
98129
});
99130
}
131+
}, {
132+
key: 'mediaQueryListener',
133+
value: function mediaQueryListener(_ref9) {
134+
var matches = _ref9.matches,
135+
media = _ref9.media;
100136

101-
// check for matches on client mount
137+
var queryName = this.reverseQueries[media];
138+
var newMedia = Object.assign({}, this.state.media, _defineProperty({}, queryName, matches));
102139

140+
if (!(0, _shallowequal2.default)(newMedia, this.state.media)) {
141+
this.setState({ media: newMedia });
142+
}
143+
}
103144
}, {
104145
key: 'render',
105146
value: function render() {
106-
return _react2.default.Fragment ? _react2.default.createElement(
107-
_react2.default.Fragment,
108-
null,
109-
this.props.children
110-
) : _react2.default.createElement(
147+
if (_react2.default.Fragment) {
148+
return _react2.default.createElement(
149+
_react2.default.Fragment,
150+
null,
151+
this.props.children
152+
);
153+
}
154+
155+
if (_react2.default.Children.count(this.props.children) > 1) {
156+
return this.props.children;
157+
}
158+
159+
return _react2.default.createElement(
111160
'div',
112161
null,
113162
this.props.children
@@ -123,13 +172,18 @@ MediaQueryProvider.childContextTypes = {
123172
};
124173

125174
MediaQueryProvider.propTypes = {
126-
queries: _propTypes2.default.object, // eslint-disable-line react/forbid-prop-types
127175
children: _propTypes2.default.node.isRequired,
176+
queries: _propTypes2.default.object, // eslint-disable-line react/forbid-prop-types
128177
values: _propTypes2.default.object // eslint-disable-line react/forbid-prop-types
129178
};
130179

131180
MediaQueryProvider.defaultProps = {
132-
queries: defaultQueries,
181+
queries: {
182+
mobile: 'screen and (max-width: 623px)',
183+
tablet: 'screen and (min-width: 624px) and (max-width: 1020px)',
184+
desktop: 'screen and (min-width: 1021px) and (max-width: 1440px)',
185+
largeDesktop: 'screen and (min-width: 1441px)'
186+
},
133187
values: {}
134188
};
135189

dist/with-media.js

+4-5
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ Object.defineProperty(exports, "__esModule", {
44
value: true
55
});
66

7-
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
8-
97
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
108

119
var _propTypes = require('prop-types');
@@ -51,7 +49,7 @@ var withMedia = function withMedia(WrappedComponent) {
5149
wrappedRef = _props.wrappedRef,
5250
otherProps = _objectWithoutProperties(_props, ['wrappedRef']);
5351

54-
return _react2.default.createElement(WrappedComponent, _extends({}, otherProps, this.state, {
52+
return _react2.default.createElement(WrappedComponent, Object.assign({}, otherProps, {
5553
media: this.context.media,
5654
ref: wrappedRef
5755
}));
@@ -70,10 +68,11 @@ var withMedia = function withMedia(WrappedComponent) {
7068
};
7169

7270
MediaQueryWrapper.defaultProps = {
73-
wrappedRef: function wrappedRef() {}
71+
wrappedRef: undefined
7472
};
7573

76-
MediaQueryWrapper.displayName = 'MediaQuery(' + getDisplayName(WrappedComponent) + ')';
74+
MediaQueryWrapper.displayName = 'withMedia(' + getDisplayName(WrappedComponent) + ')';
75+
7776
return (0, _hoistNonReactStatics2.default)(MediaQueryWrapper, WrappedComponent);
7877
};
7978

package.json

+23-25
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
"main": "dist/index.js",
66
"scripts": {
77
"prepublish": "npm run build",
8-
"build": "mkdir -p dist && babel src --out-dir dist",
9-
"test": "mocha test",
10-
"lint": "eslint ./src"
8+
"build": "NODE_ENV=production babel --out-dir dist srcy",
9+
"test": "NODE_ENV=test mocha test",
10+
"tdd": "NODE_ENV=test mocha --watch test",
11+
"lint": "eslint ."
1112
},
1213
"keywords": [
1314
"react",
@@ -28,48 +29,45 @@
2829
"url": "https://github.com/DomainGroupOSS/react-media-query-hoc/issues"
2930
},
3031
"homepage": "https://github.com/DomainGroupOSS/react-media-query-hoc#readme",
31-
"eslintConfig": {
32-
"extends": "airbnb",
33-
"rules": {
34-
"react/jsx-filename-extension": "off"
35-
}
36-
},
3732
"babel": {
3833
"presets": [
39-
"es2015",
40-
"stage-0",
41-
"react"
34+
"react-app"
35+
],
36+
"plugins": [
37+
"transform-es2015-modules-commonjs"
4238
]
4339
},
4440
"peerDependencies": {
45-
"react": "^15.1.0"
41+
"react": "^15.1.0 || ^16.0.0"
4642
},
4743
"devDependencies": {
4844
"babel-cli": "^6.10.1",
49-
"babel-preset-es2015": "^6.9.0",
50-
"babel-preset-react": "^6.5.0",
51-
"babel-preset-stage-0": "^6.5.0",
45+
"babel-preset-react-app": "^3.1.1",
5246
"babel-register": "^6.9.0",
5347
"chai": "^4.1.2",
5448
"enzyme": "^2.3.0",
55-
"eslint": "^3.19.0",
56-
"eslint-config-airbnb": "^15.0.1",
57-
"eslint-plugin-import": "^2.3.0",
58-
"eslint-plugin-jsx-a11y": "^5.0.3",
59-
"eslint-plugin-react": "^7.0.1",
49+
"eslint": "^4.19.1",
50+
"eslint-config-airbnb": "^16.1.0",
51+
"eslint-plugin-import": "^2.11.0",
52+
"eslint-plugin-jsx-a11y": "^6.0.3",
53+
"eslint-plugin-react": "^7.8.2",
6054
"jsdom": "^11.0.0",
6155
"match-media-mock": "^0.1.0",
6256
"mocha": "^2.5.3",
63-
"proxyquire": "^1.8.0",
6457
"react": "^15.1.0",
6558
"react-addons-test-utils": "^15.1.0",
6659
"react-dom": "^15.1.0",
67-
"sinon": "^3.2.1"
60+
"sinon": "^5.0.7"
6861
},
6962
"dependencies": {
63+
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
64+
"css-mediaquery": "^0.1.2",
7065
"hoist-non-react-statics": "^1.2.0",
71-
"matchmedia": "^0.1.2",
7266
"prop-types": "^15.5.10",
7367
"shallowequal": "^1.0.2"
74-
}
68+
},
69+
"browserslist": [
70+
"> 1%",
71+
"IE 10"
72+
]
7573
}

0 commit comments

Comments
 (0)