From 56bf7f11bf297bc5953c70cbd56628876bcca916 Mon Sep 17 00:00:00 2001 From: jonathan schatz Date: Wed, 13 Sep 2017 13:32:19 -0700 Subject: [PATCH] initial version? --- LICENSE | 2 +- README.md | 255 +++++++++++++++++++++++++++++++++++++++++++- es/defaults.js | 5 +- es/defaults.js.map | 2 +- lib/defaults.js | 5 +- lib/defaults.js.map | 2 +- package.json | 12 ++- src/defaults.js | 5 +- 8 files changed, 270 insertions(+), 18 deletions(-) diff --git a/LICENSE b/LICENSE index e503d34..0c0557d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 jonathan schatz +Copyright (c) 2017 jonathan schatz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 6604362..169c8f4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,49 @@ # react-responsive-redux [![npm version](https://badge.fury.io/js/react-responsive-redux.svg)](https://badge.fury.io/js/react-responsive-redux) -[Redux](http://redux.js.org/) integration for [react-responsive](https://github.com/contra/react-responsive) +## The Problem +If you use [react-responsive](https://github.com/contra/react-responsive) and [server-side-rendering](https://facebook.github.io/react/docs/react-dom-server.html) you've probably come across this cryptic warning in your browser console before: + +> Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server: (client) actid="1">Foo</h1><div data-react (server) actid="1">Bar</h1><div + +This happens when the server and client disagree about the state of the DOM. With [react-responsive](https://github.com/contra/react-responsive) specifically it can be caused by code like this: +```javascript + +
You are a mobile device
+
+ +
You are a desktop
+
+``` +On the client side this works just fine, but on the server side it doesn't because there's no DOM to query, so both components render empty on the server. Once `react` takes over on the client side it will re-render these elements correctly based on the provided media queries. + + [react-responsive](https://github.com/contra/react-responsive) offers the [values prop](https://github.com/contra/react-responsive#server-rendering) as a workaround: +```javascript + +
You are a mobile device
+
+ +
You are a desktop
+
+``` + +This example will work fine as long as your browser window is `>= 1024px`. If it's not you're back to the warning above. + +### A Solution + +`react-responsive-redux` combines several pieces together to work around this issue. +1. The `User-Agent` string is sniffed to get a reasonable guess of the client's screen size +2. This value is stored in [redux](http://redux.js.org/) store so it's globally accessible +3. Wrapped versions of [MediaQuery](https://github.com/contra/react-responsive#using-css-media-queries) are provided which get their `width` from the global store. + +The end result is that it's possible to do server-side rendering correctly for responsive pages which change for mobile, tablet, or desktop users (it also means that `react` warning probably disappears): +```javascript + +
You are a mobile device
+
+ +
You are a desktop
+
+``` ## Installation Install with [npm](https://www.npmjs.com/): @@ -9,5 +52,213 @@ npm install react-responsive-redux ``` ## Usage +To use `react-responsive-redux` you need to do the following: +1. Add a redux reducer to your store +2. Add mobile detection and an action dispatch in your request handler +3. Use a wrapped component + +### redux setup +Add the `responsive` reducer into your `redux` store: + +#### ES5 Example +```javascript +var redux = require('redux') +var responsiveReducer = require('react-responsive-redux').reducer + +var reducers = { + // ... your other reducers here ... + responsive: responsiveReducer +} +var reducer = redux.combineReducers(reducers) +var store = redux.createStore(reducer) +``` + +#### ES6 Example +```javascript +import { createStore, combineReducers } from 'redux' +import { reducer as responsiveReducer } from 'react-responsive-redux' + +const reducers = { + // ... your other reducers here ... + responsive: responsiveReducer +} +const reducer = combineReducers(reducers) +const store = createStore(reducer) +``` + +### server setup +Add mobile detection and dispatch a redux action during your request handler. This must happen after you've created your `redux` store and before you do your server-side rendering. + +(Note: these examples are based on the [redux SSR example](http://redux.js.org/docs/recipes/ServerRendering.html#the-server-side) and are incomplete as-is). +#### ES5 Example +```javascript +var setMobileDetect = require('react-responsive-redux').setMobileDetect +var mobileParser = require('react-responsive-redux').mobileParser +var reducers = require('./reducers') +var renderToString = require('react-dom/server').renderToString + +function handleRender(req, res) { + // Create a new Redux store instance + var store = createStore(reducers) + var dispatch = store.dispatch + + // do our mobile detection + var mobileDetect = mobileParser(req) + + // set mobile detection for our responsive store + dispatch(setMobileDetect(mobileDetect)) + + // Render the component to a string + var html = renderToString( + + + + ) + + // Grab the initial state from our Redux store + var preloadedState = store.getState() + + // Send the rendered page back to the client + res.send(renderFullPage(html, preloadedState)) +} +``` + +#### ES6 Example +```javascript +import { setMobileDetect, mobileParser } from 'react-responsive-redux' +import reducers from './reducers' +import { renderToString } from 'react-dom/server' + +function handleRender(req, res) { + // Create a new Redux store instance + const store = createStore(reducers) + const { dispatch } = store + + // do our mobile detection + const mobileDetect = mobileParser(req) + + // set mobile detection for our responsive store + dispatch(setMobileDetect(mobileDetect)) + + // Render the component to a string + const html = renderToString( + + + + ) + + // Grab the initial state from our Redux store + const preloadedState = store.getState() + + // Send the rendered page back to the client + res.send(renderFullPage(html, preloadedState)) +} +``` +### React Components + +#### responsiveWrapper(props={}) +`responsiveWrapper` is a wrapper to generate [redux](http://redux.js.org/)-connected [MediaQuery](https://github.com/contra/react-responsive#using-css-media-queries) components. These components have `width` and `deviceWidth` set in the [values prop](https://github.com/contra/react-responsive#server-rendering) from the connected store. All props are passed through to the underlying ` + +
You are a mobile device
+
+ +
You are a desktop
+
+ + ) +} + +``` + +#### ES6 Example +```javascript +import React from 'react' +import { MobileScreen, DesktopScreen } from 'react-responsive-redux' + +const Component = () => +
+ +
You are a mobile device
+
+ +
You are a desktop
+
+
+``` + +### Breakpoints +The current breakpoints are based on [bootstrap's](https://v4-alpha.getbootstrap.com/layout/overview/#responsive-breakpoints) sizings. + +|Device Type| Breakpoint | +|-----------|-----| +| Phone | max-width: 767px | +| Tablet | min-width: 768px, max-width: 991px | +| Mobile | max-width: 991px | +| Desktop | min-width: 992px | +----------------- + +### Fake Widths +Device detection is done using [mobile-detect](https://github.com/hgoebl/mobile-detect.js) which allows us to get the following information: + + * `mobile` - is the device mobile (ie, `phone` or `tablet`) + * `phone` - is the device a phone? + * `tablet` - is the device a tablet? + * `desktop` - the opposite of `mobile` + +Based on this information and our breakpoints we set a fake screen size in our store: + +|Device Type| Fake Width (in px)| +|-----------|-----| +| Phone | 767 | +| Tablet | 991 | +| Mobile | 767 | +| Desktop | 992 | +----------------- -### Options +### TODO + * Add support for custom breakpoints + * Add support for custom screen sizes diff --git a/es/defaults.js b/es/defaults.js index 3be0334..1f77da1 100644 --- a/es/defaults.js +++ b/es/defaults.js @@ -3,9 +3,8 @@ // > phone && <= tablet is a tablet, and > tablet is a desktop export var breakPoints = { // the phone value covers portrait and landscape - there's no way to tell the - // difference from the request unless we have client hints (which don't work - // on the first - // request anyway) or something similar + // difference from the request unless we have client hints (which don't work + // on the first request anyway) or something similar phone: 767, // this is tricky too - we're going by what bootstrap uses as a default but // an ipad in portrait mode will match here even though the width might be diff --git a/es/defaults.js.map b/es/defaults.js.map index 68f0de0..92b317b 100644 --- a/es/defaults.js.map +++ b/es/defaults.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/defaults.js"],"names":["breakPoints","phone","tablet"],"mappings":"AAAA;AACA;AACA;AACA,OAAO,IAAMA,cAAc;AACzB;AACA;AACA;AACA;AACAC,SAAO,GALkB;AAMzB;AACA;AACA;AACA;AACAC,UAAQ;AAViB,CAApB","file":"defaults.js","sourcesContent":["// based on bootstrap and http://www.websitedimensions.com/\n// these are the maximum values for the device type so <= phone is a phone,\n// > phone && <= tablet is a tablet, and > tablet is a desktop\nexport const breakPoints = {\n // the phone value covers portrait and landscape - there's no way to tell the\n // difference from the request unless we have client hints (which don't work\n // on the first\n // request anyway) or something similar\n phone: 767,\n // this is tricky too - we're going by what bootstrap uses as a default but\n // an ipad in portrait mode will match here even though the width might be\n // 1024, 1112, or 1366. for now leave as is - in the future we could handle\n // this by compiling a list of resolutions but that's a huge undertaking.\n tablet: 991,\n}\n"]} \ No newline at end of file +{"version":3,"sources":["../src/defaults.js"],"names":["breakPoints","phone","tablet"],"mappings":"AAAA;AACA;AACA;AACA,OAAO,IAAMA,cAAc;AACzB;AACA;AACA;AACAC,SAAO,GAJkB;AAKzB;AACA;AACA;AACA;AACAC,UAAQ;AATiB,CAApB","file":"defaults.js","sourcesContent":["// based on bootstrap and http://www.websitedimensions.com/\n// these are the maximum values for the device type so <= phone is a phone,\n// > phone && <= tablet is a tablet, and > tablet is a desktop\nexport const breakPoints = {\n // the phone value covers portrait and landscape - there's no way to tell the\n // difference from the request unless we have client hints (which don't work\n // on the first request anyway) or something similar\n phone: 767,\n // this is tricky too - we're going by what bootstrap uses as a default but\n // an ipad in portrait mode will match here even though the width might be\n // 1024, 1112, or 1366. for now leave as is - in the future we could handle\n // this by compiling a list of resolutions but that's a huge undertaking.\n tablet: 991,\n}\n"]} \ No newline at end of file diff --git a/lib/defaults.js b/lib/defaults.js index e47133c..14e0a2a 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -8,9 +8,8 @@ Object.defineProperty(exports, "__esModule", { // > phone && <= tablet is a tablet, and > tablet is a desktop var breakPoints = exports.breakPoints = { // the phone value covers portrait and landscape - there's no way to tell the - // difference from the request unless we have client hints (which don't work - // on the first - // request anyway) or something similar + // difference from the request unless we have client hints (which don't work + // on the first request anyway) or something similar phone: 767, // this is tricky too - we're going by what bootstrap uses as a default but // an ipad in portrait mode will match here even though the width might be diff --git a/lib/defaults.js.map b/lib/defaults.js.map index 188d0d8..64fb3ea 100644 --- a/lib/defaults.js.map +++ b/lib/defaults.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/defaults.js"],"names":["breakPoints","phone","tablet"],"mappings":";;;;;AAAA;AACA;AACA;AACO,IAAMA,oCAAc;AACzB;AACA;AACA;AACA;AACAC,SAAO,GALkB;AAMzB;AACA;AACA;AACA;AACAC,UAAQ;AAViB,CAApB","file":"defaults.js","sourcesContent":["// based on bootstrap and http://www.websitedimensions.com/\n// these are the maximum values for the device type so <= phone is a phone,\n// > phone && <= tablet is a tablet, and > tablet is a desktop\nexport const breakPoints = {\n // the phone value covers portrait and landscape - there's no way to tell the\n // difference from the request unless we have client hints (which don't work\n // on the first\n // request anyway) or something similar\n phone: 767,\n // this is tricky too - we're going by what bootstrap uses as a default but\n // an ipad in portrait mode will match here even though the width might be\n // 1024, 1112, or 1366. for now leave as is - in the future we could handle\n // this by compiling a list of resolutions but that's a huge undertaking.\n tablet: 991,\n}\n"]} \ No newline at end of file +{"version":3,"sources":["../src/defaults.js"],"names":["breakPoints","phone","tablet"],"mappings":";;;;;AAAA;AACA;AACA;AACO,IAAMA,oCAAc;AACzB;AACA;AACA;AACAC,SAAO,GAJkB;AAKzB;AACA;AACA;AACA;AACAC,UAAQ;AATiB,CAApB","file":"defaults.js","sourcesContent":["// based on bootstrap and http://www.websitedimensions.com/\n// these are the maximum values for the device type so <= phone is a phone,\n// > phone && <= tablet is a tablet, and > tablet is a desktop\nexport const breakPoints = {\n // the phone value covers portrait and landscape - there's no way to tell the\n // difference from the request unless we have client hints (which don't work\n // on the first request anyway) or something similar\n phone: 767,\n // this is tricky too - we're going by what bootstrap uses as a default but\n // an ipad in portrait mode will match here even though the width might be\n // 1024, 1112, or 1366. for now leave as is - in the future we could handle\n // this by compiling a list of resolutions but that's a huge undertaking.\n tablet: 991,\n}\n"]} \ No newline at end of file diff --git a/package.json b/package.json index 88c35e7..81a065e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-responsive-redux", - "version": "0.0.1", + "version": "0.1.0", "description": "redux integration for react-responsive", "main": "lib/index.js", "module": "es/index.js", @@ -9,15 +9,19 @@ "lib/*", "es/*" ], - "homepage": "https://github.com/modosc/react-resonsive-redux", + "homepage": "https://github.com/modosc/react-responsive-redux", "bugs": { - "url": "https://github.com/modosc/react-resonsive-redux/issues" + "url": "https://github.com/modosc/react-responsive-redux/issues" }, "keywords": [ "react", "react-responsive", "responsive", - "redux" + "redux", + "ssr", + "server", + "side", + "rendering" ], "scripts": { "test": "cross-env BABEL_ENV=commonjs mocha --compilers js:babel-register --require babel-polyfill --require test/setup.js test/*.test.js", diff --git a/src/defaults.js b/src/defaults.js index 2eee279..8878b59 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -3,9 +3,8 @@ // > phone && <= tablet is a tablet, and > tablet is a desktop export const breakPoints = { // the phone value covers portrait and landscape - there's no way to tell the - // difference from the request unless we have client hints (which don't work - // on the first - // request anyway) or something similar + // difference from the request unless we have client hints (which don't work + // on the first request anyway) or something similar phone: 767, // this is tricky too - we're going by what bootstrap uses as a default but // an ipad in portrait mode will match here even though the width might be