client.js (5954B)
1 /** 2 * React Starter Kit (https://www.reactstarterkit.com/) 3 * 4 * Copyright © 2014-present Kriasoft, LLC. All rights reserved. 5 * 6 * This source code is licensed under the MIT license found in the 7 * LICENSE.txt file in the root directory of this source tree. 8 */ 9 10 import React from 'react'; 11 import ReactDOM from 'react-dom'; 12 import FastClick from 'fastclick'; 13 import UniversalRouter from 'universal-router'; 14 import queryString from 'query-string'; 15 import { createPath } from 'history/PathUtils'; 16 import history from './core/history'; 17 import App from './components/App'; 18 import { updateMeta } from './core/DOMUtils'; 19 import { ErrorReporter, deepForceUpdate } from './core/devUtils'; 20 21 // Global (context) variables that can be easily accessed from any React component 22 // https://facebook.github.io/react/docs/context.html 23 const context = { 24 // Enables critical path CSS rendering 25 // https://github.com/kriasoft/isomorphic-style-loader 26 insertCss: (...styles) => { 27 // eslint-disable-next-line no-underscore-dangle 28 const removeCss = styles.map(x => x._insertCss()); 29 return () => { removeCss.forEach(f => f()); }; 30 }, 31 }; 32 33 // Switch off the native scroll restoration behavior and handle it manually 34 // https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration 35 const scrollPositionsHistory = {}; 36 if (window.history && 'scrollRestoration' in window.history) { 37 window.history.scrollRestoration = 'manual'; 38 } 39 40 let onRenderComplete = function initialRenderComplete() { 41 const elem = document.getElementById('css'); 42 if (elem) elem.parentNode.removeChild(elem); 43 onRenderComplete = function renderComplete(route, location) { 44 document.title = route.title; 45 46 updateMeta('description', route.description); 47 // Update necessary tags in <head> at runtime here, ie: 48 // updateMeta('keywords', route.keywords); 49 // updateCustomMeta('og:url', route.canonicalUrl); 50 // updateCustomMeta('og:image', route.imageUrl); 51 // updateLink('canonical', route.canonicalUrl); 52 // etc. 53 54 let scrollX = 0; 55 let scrollY = 0; 56 const pos = scrollPositionsHistory[location.key]; 57 if (pos) { 58 scrollX = pos.scrollX; 59 scrollY = pos.scrollY; 60 } else { 61 const targetHash = location.hash.substr(1); 62 if (targetHash) { 63 const target = document.getElementById(targetHash); 64 if (target) { 65 scrollY = window.pageYOffset + target.getBoundingClientRect().top; 66 } 67 } 68 } 69 70 // Restore the scroll position if it was saved into the state 71 // or scroll to the given #hash anchor 72 // or scroll to top of the page 73 window.scrollTo(scrollX, scrollY); 74 75 // Google Analytics tracking. Don't send 'pageview' event after 76 // the initial rendering, as it was already sent 77 if (window.ga) { 78 window.ga('send', 'pageview', createPath(location)); 79 } 80 }; 81 }; 82 83 // Make taps on links and buttons work fast on mobiles 84 FastClick.attach(document.body); 85 86 const container = document.getElementById('app'); 87 let appInstance; 88 let currentLocation = history.location; 89 let routes = require('./routes').default; 90 91 // Re-render the app when window.location changes 92 async function onLocationChange(location, action) { 93 // Remember the latest scroll position for the previous location 94 scrollPositionsHistory[currentLocation.key] = { 95 scrollX: window.pageXOffset, 96 scrollY: window.pageYOffset, 97 }; 98 // Delete stored scroll position for next page if any 99 if (action === 'PUSH') { 100 delete scrollPositionsHistory[location.key]; 101 } 102 currentLocation = location; 103 104 try { 105 // Traverses the list of routes in the order they are defined until 106 // it finds the first route that matches provided URL path string 107 // and whose action method returns anything other than `undefined`. 108 const route = await UniversalRouter.resolve(routes, { 109 path: location.pathname, 110 query: queryString.parse(location.search), 111 }); 112 113 // Prevent multiple page renders during the routing process 114 if (currentLocation.key !== location.key) { 115 return; 116 } 117 118 if (route.redirect) { 119 history.replace(route.redirect); 120 return; 121 } 122 123 appInstance = ReactDOM.render( 124 <App context={context}>{route.component}</App>, 125 container, 126 () => onRenderComplete(route, location), 127 ); 128 } catch (error) { 129 // Display the error in full-screen for development mode 130 if (__DEV__) { 131 appInstance = null; 132 document.title = `Error: ${error.message}`; 133 ReactDOM.render(<ErrorReporter error={error} />, container); 134 throw error; 135 } 136 137 console.error(error); // eslint-disable-line no-console 138 139 // Do a full page reload if error occurs during client-side navigation 140 if (action && currentLocation.key === location.key) { 141 window.location.reload(); 142 } 143 } 144 } 145 146 // Handle client-side navigation by using HTML5 History API 147 // For more information visit https://github.com/mjackson/history#readme 148 history.listen(onLocationChange); 149 onLocationChange(currentLocation); 150 151 // Handle errors that might happen after rendering 152 // Display the error in full-screen for development mode 153 if (__DEV__) { 154 window.addEventListener('error', (event) => { 155 appInstance = null; 156 document.title = `Runtime Error: ${event.error.message}`; 157 ReactDOM.render(<ErrorReporter error={event.error} />, container); 158 }); 159 } 160 161 // Enable Hot Module Replacement (HMR) 162 if (module.hot) { 163 module.hot.accept('./routes', () => { 164 routes = require('./routes').default; // eslint-disable-line global-require 165 166 if (appInstance) { 167 try { 168 // Force-update the whole tree, including components that refuse to update 169 deepForceUpdate(appInstance); 170 } catch (error) { 171 appInstance = null; 172 document.title = `Hot Update Error: ${error.message}`; 173 ReactDOM.render(<ErrorReporter error={error} />, container); 174 return; 175 } 176 } 177 178 onLocationChange(currentLocation); 179 }); 180 }