UNPKG

react-broker

Version:

Component lazy-loading + code splitting that works with SSR, Webpack 4, and Babel 7

353 lines (305 loc) 9.93 kB
function _extends() { _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 } return _extends.apply(this, arguments) } import React, { useContext, useEffect, useCallback, useMemo, useReducer, } from 'react' import {ServerPromisesContext, loadPromises} from '@react-hook/server-promises' import {getChunkScripts, findChunks} from './utils' // context is necessary for keeping track of which components in the // current react tree depend on which corresponding chunks/promises const BrokerContext = React.createContext({}), WAITING = 0, // promise has not yet started loading LOADING = 1, // promise has started loading REJECTED = -1, // promise was rejected RESOLVED = 3 // promise was successfully resolved // tracks the chunks used in the rendering of a tree const createChunkCache = () => { let map = {}, cache = { get: k => map[k], set: (k, v) => (map[k] = v), invalidate: k => delete map[k], // returns an array of chunk names used by the current react tree getChunkNames: () => Object.keys(map), // returns a Set of Webpack chunk objects used by the current react tree getChunks: stats => findChunks(stats, cache.getChunkNames()), // returns a string of <script> tags for Webpack chunks used by the // current react tree getChunkScripts: (stats, opt = {}) => getChunkScripts(stats, cache, opt), } if (process.env.NODE_ENV !== 'production') cache.forEach = fn => Object.keys(map).forEach(k => fn(k, map[k])) return cache } // loads an array of Lazy components function _ref(i) { return i.load() } const load = (...instances) => Promise.all(instances.map(_ref)) // resolves components on the server const loadAll = loadPromises // the purpose of this function is to avoid a flash or loading // spinner when your app initially hydrates/renders const loadInitial = (chunkCache = globalChunkCache) => { let chunks = document.getElementById('__INITIAL_BROKER_CHUNKS__') if (!chunks) { throw new Error( 'No chunk cache element was found at <script id="__INITIAL_BROKER_CHUNKS__">' ) } chunks = JSON.parse(chunks.firstChild.data) const loading = [] // preloads the chunk scripts for (let script of document.querySelectorAll('script[data-rb]')) { loading.push( new Promise(resolve => { if (script.getAttribute('data-loaded')) resolve() else script.addEventListener('load', resolve) }) ) } function _ref2(chunkName) { let component try { component = __webpack_require__(chunks[chunkName]).default } finally { // sets the component in the chunk cache if it is valid if (typeof component === 'function') { if (typeof module !== 'undefined' && module.hot) __webpack_require__.c[chunks[chunkName]].hot.accept() chunkCache.set(chunkName, { status: RESOLVED, component, }) } } } return Promise.all(loading).then(() => Object.keys(chunks).forEach(_ref2)) } const globalChunkCache = createChunkCache(), childContextDispatcher = (state, {chunkName, chunk}) => { state.chunks.set(chunkName, chunk) return _extends({}, state) } function _ref6(chunkName, chunk) { if (chunk.status !== WAITING && chunk.status !== LOADING) { chunk.status = WAITING console.log(' -', chunkName) } } const Provider = ({ children, chunkCache = globalChunkCache, ssrContext = ServerPromisesContext, }) => { const context = useContext(ssrContext) const resolved = useCallback( (chunkName, component) => { // updates a chunk's consumers when the chunk is resolved and sets the // chunk status to 'RESOLVED' const chunk = chunkCache.get(chunkName) if (chunk.status !== RESOLVED) { chunk.status = RESOLVED // modules typically resolve with a 'default' attribute, but some don't. // likewise, fetch() never resolves with a 'default' attribute. chunk.component = component && component.default !== void 0 ? component.default : component // updates each chunk listener with the resolved component dispatchChildContext({ chunkName, chunk, }) } return chunk.component }, [chunkCache] ) const rejected = useCallback( (chunkName, error) => { // updates a chunk's consumers when the chunk is rejected and sets the // chunk status to 'REJECTED' const chunk = chunkCache.get(chunkName) if (chunk.status !== REJECTED) { chunk.status = REJECTED chunk.error = error // updates each chunk listener with the caught error dispatchChildContext({ chunkName, chunk, }) } return error }, [chunkCache] ) const load = useCallback( (chunkName, promise) => { // loads a given chunk and updates its consumers when it is resolved or // rejected. also sets the chunk's status to 'LOADING' if it hasn't // already resolved const chunk = chunkCache.get(chunkName) function _ref3(component) { return resolved(chunkName, component) } function _ref4(err) { return rejected(chunkName, err) } if (chunk.status === WAITING) { // tells registered components that we've started loading this chunk chunk.promise = promise.then(_ref3).catch(_ref4) chunk.status = LOADING dispatchChildContext({ chunkName, chunk, }) } return chunk.promise }, [chunkCache, resolved, rejected] ) const add = useCallback( (chunkName, promise) => { const chunk = chunkCache.get(chunkName) if (chunk === void 0 || chunk.status === WAITING) { // adds the chunk to the chunk cache chunkCache.set(chunkName, { status: WAITING, }) promise = promise() if (context) context.push(promise) return load(chunkName, promise) } else { // this chunk has already resolved return chunk.promise } }, [chunkCache, load, ssrContext] ) const initialState = () => ({ load, add, chunks: chunkCache, }), [childContext, dispatchChildContext] = useReducer( childContextDispatcher, null, initialState ) function _ref7(status) { if (status === 'idle') { // fetches any preloaded chunks console.log('[Broker HMR] reloading') let chunks = document.getElementById('__INITIAL_BROKER_CHUNKS__') function _ref5(chunkName) { if (typeof module !== 'undefined' && module.hot) { try { __webpack_require__(chunks[chunkName]).default } finally { const chunk = chunkCache.get(chunkName) chunk.status = WAITING load( chunkName, Promise.resolve(__webpack_require__.c[chunks[chunkName]].exports) ) __webpack_require__.c[chunks[chunkName]].hot.accept() console.log(' -', chunkName) } } } if (chunks) { // initial chunks were loaded and we need this workaround to get them to // refresh for some reason chunks = JSON.parse(chunks.firstChild.data) Object.keys(chunks).forEach(_ref5) } } else if (status === 'apply') { chunkCache.forEach(_ref6) } } function _ref8() { let invalidateChunks if (typeof module !== 'undefined' && module.hot) { invalidateChunks = _ref7 module.hot.addStatusHandler(invalidateChunks) } return () => invalidateChunks && module.hot.removeStatusHandler(invalidateChunks) } if (process.env.NODE_ENV !== 'production') { useEffect(_ref8, [load, chunkCache]) } return React.createElement(BrokerContext.Provider, { value: childContext, children: children, }) } const BrokerProvider = Provider, defaultOpt = { loading: null, error: null, }, emptyArr = [] const lazy = (chunkName, promise, opt = defaultOpt) => { const Lazy = props => { const broker = useContext(BrokerContext), load = useCallback(() => broker.load(chunkName, promise()), emptyArr), chunk = broker.chunks.get(chunkName) useMemo(() => broker.add(chunkName, promise), emptyArr) let render switch (chunk === null || chunk === void 0 ? void 0 : chunk.status) { case REJECTED: // returns 'error' component // if there isn't an explicitly defined 'error' component, the // 'loading' component will be used as a backup with the error // message passed in the second argument render = opt.error || opt.loading return render ? render(props, { retry: load, error: chunk.error, }) : null case RESOLVED: // returns the proper resolved component return React.createElement(chunk.component, props) default: // returns 'loading' component return opt.loading ? opt.loading(props, { retry: load, error: null, }) : null } } // necessary for calling Component.load from the application code Lazy.load = () => promise() // <Lazy(pages/Home)> makes visual grep'ing easier in react-dev-tools if (process.env.NODE_ENV !== 'production') Lazy.displayName = 'Lazy(' + chunkName + ')' return Lazy } export { Provider, BrokerProvider, lazy, load, loadAll, loadInitial, createChunkCache, findChunks, getChunkScripts, }