This is a very simple approach using matchMedia support. We store media queries and a listener function as an object in an array. When the media query returns true the listener function will set the relevant component state true.
In our actual project, this approach is used with Redux. The queries instead of setting component state, update the Store. Containers connected to this store send the breakPoint, orientation, and other props to each child component.
Just an experiment. If pursued further, would probably make it a HOC (Higher Order Component).
Gist available here.
Demo
If packed and loaded into a web page as we resize the browser we should see:
Change size of browser to test state: { "orientation": "landscape", "breakPoint": "LG" }
Why?
While CSS media queries are very useful, sometimes being able to respond ‘behaviorally’ to media events is very powerful.
Tested on
Chrome, FireFox, IE, and Edge. I guess they all have JavaScript MediaQuery support.
How it works
In the life cycle method ‘componentDidMount’ we add a query to an array in the component:
this.mediaQueryFactory(obj, '(orientation: portrait)', this.onPortrait); this.mediaQueryLists.push({ mql: obj.mql, listener: this.onPortrait });
the onPortrait is the listener function:
onPortrait : function(flag){ if(flag){ this.setState({orientation: 'portrait'}); } },
Full Code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import {render} from 'react-dom'; | |
import "babel-polyfill"; | |
import invariant from "invariant"; | |
/** | |
* A P P | |
* | |
*/ | |
var App = React.createClass({ | |
/** | |
* Array of objects to contain the mediaQueryLists objects | |
* | |
* Each object is composed of: | |
* { | |
* mql: mediaQueryList, | |
* listener: function | |
} } | |
* | |
*/ | |
mediaQueryLists : [], | |
/** | |
* Simple demo, dumps State to UI. | |
*/ | |
render: function(){ | |
return( | |
<div id="main" style={{padding:'4em'}}> | |
<h1 className="disclaimer">Change size of browser to test</h1> | |
<h2>{`state: ${JSON.stringify(this.state,null,2)}`}</h2> | |
</div> | |
); | |
}, | |
/** | |
* | |
*/ | |
componentDidMount : function(){ | |
var obj = {}; // obj to use for passing mql to List of mql. | |
// TODO: these CSS strings should come from properties | |
this.mediaQueryFactory(obj, '(orientation: portrait)', this.onPortrait); | |
this.mediaQueryLists.push({ | |
mql: obj.mql, | |
listener: this.onPortrait | |
}); | |
this.mediaQueryFactory(obj, '(orientation: landscape)', this.onLandscape); | |
this.mediaQueryLists.push({ | |
mql: obj.mql, | |
listener: this.onLandscape | |
}); | |
this.mediaQueryFactory(obj, '(max-width: 767px)', this.onXS); | |
this.mediaQueryLists.push({ | |
mql: obj.mql, | |
listener: this.onXS | |
}); | |
this.mediaQueryFactory(obj, '(min-width:768px) and (max-width: 992px)', this.onSM); | |
this.mediaQueryLists.push({ | |
mql: obj.mql, | |
listener: this.onSM | |
}); | |
this.mediaQueryFactory(obj, '(min-width:993px) and (max-width: 1199px)', this.onMD); | |
this.mediaQueryLists.push({ | |
mql: obj.mql, | |
listener: this.onMD | |
}); | |
this.mediaQueryFactory(obj, 'screen and (min-width:1200px)', this.onLG); | |
this.mediaQueryLists.push({ | |
mql: obj.mql, | |
listener: this.onLG | |
}); | |
}, | |
/** | |
* | |
*/ | |
componentWillUnMount : function(){ | |
this.mediaQueryLists.forEach( (mql) => { | |
mql.mql.removeListener(mql.listener); | |
}); | |
this.mediaQueryLists.length = 0; // clear the array. | |
}, | |
/** | |
* Responsive media support | |
* | |
* Create mediaQueryList object. | |
* | |
* See http://devdocs.io/dom/window/matchmedia | |
* See https://github.com/weblinc/media-match | |
* | |
* @param obj – obj to store the new mql | |
* @param String query – the CSS query | |
* @param func – the callBack function | |
* @return nothing | |
*/ | |
mediaQueryFactory : function(obj, query, func){ | |
var mql = window.matchMedia(query); | |
var listener = function(x){ | |
func(x.matches); | |
} | |
mql.addListener(listener); | |
listener(mql); | |
obj.mql = mql; | |
}, | |
/** | |
*/ | |
onPortrait : function(flag){ | |
if(flag){ | |
this.setState({orientation: 'portrait'}); | |
} | |
}, | |
/** | |
*/ | |
onLandscape : function(flag){ | |
if(flag){ | |
this.setState({orientation: 'landscape'}); | |
} | |
}, | |
/** | |
*/ | |
onXS : function(flag){ | |
if(flag){ | |
this.setState({breakPoint:'XS'}); | |
} | |
}, | |
/** | |
*/ | |
onSM : function(flag){ | |
if(flag){ | |
this.setState({breakPoint:'SM'}); | |
} | |
}, | |
/** | |
*/ | |
onMD : function(flag){ | |
if(flag){ | |
this.setState({breakPoint:'MD'}); | |
} | |
}, | |
/** | |
*/ | |
onLG : function(flag){ | |
if(flag){ | |
this.setState({breakPoint:'LG'}); | |
} | |
}, | |
}); | |
/* | |
* Render the app on the page. | |
*/ | |
render(<App />, document.getElementById('root')); | |
// end of index.js |
Other MediaQuery React implementations
- Window.matchMedia(); https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia
- react-responsive; https://github.com/contra/react-responsive
- Can I use matchMedia; http://caniuse.com/#search=matchMedia
- recompose; https://github.com/acdlite/recompose
- React Higher Order Components in depth; https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3e#.p37g65nu4
Addendum
{ "name": "ReactMediaQuery", "version": "1.0.0", "description": "Attempt at using Media Queries in React", "main": "./index.js", "scripts": { "test": "echo "Error: no test specified" && exit 1", "start": "webpack index.js dist/react-media-query-bundle.js" }, "keywords": [ "react" ], "author": "JosefBetancourt", "license": "SEE LICENSE IN COPYWRITE.TXT", "dependencies": { "babel-polyfill": "^6.9.1", "classnames": "^2.2.5", "react": "^15.3.0", "react-dom": "^15.3.0" }, "devDependencies": { "babel-core": "^6.10.4", "babel-loader": "^6.2.4", "babel-plugin-add-module-exports": "^0.2.1", "babel-preset-es2015": "^6.9.0", "babel-preset-es2016": "^6.11.3", "babel-preset-react": "^6.3.13", "babel-preset-stage-2": "^6.11.0", "eslint": "^3.2.2", "webpack": "^1.13.1" } }
package.json
var path = require('path'); const webpack = require('webpack'); module.exports = { /* entry point */ entry: './index.js', /* output folder */ output: { filename: 'dist/react-media-query-bundle.js' }, devtool: 'source-map', module: { loaders: [ { test: /.js$/, loader: 'babel-loader', exclude: /node_modules/, query: { presets: ['es2015', 'react', 'stage-2'] } }, ] }, eslint: { configFile: path.join(__dirname, 'eslint.js'), useEslintrc: false }, plugins: [ ], resolve: { extensions: ['', '.js', '.json'] }, resolveLoader: { modulesDirectories: [ 'node_modules' ] } };
webpack.config.js
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>React Media Queries</title> </head> <body class="body"> <div id="root" class="">Loading page, please wait ....</div> <script src="dist/react-media-query-bundle.js"></script> </body> </html>
index.html