From 29a360eeb5fde3e55aa6011d6a069518b8d6d607 Mon Sep 17 00:00:00 2001 From: Daniel Tijerina Date: Sun, 1 Nov 2020 01:43:32 -0500 Subject: [PATCH] feat: Upgrade React Context API (#216) --- README.md | 8 ++--- __tests__/index.js | 7 ++-- __tests__/requireUniversalModule.js | 10 +++--- __tests__/utils.js | 5 ++- src/context.js | 5 +++ src/flowTypes.js | 11 +++--- src/index.js | 55 +++++++++++++---------------- src/report-chunks.js | 18 +++++----- src/requireUniversalModule.js | 25 +++---------- src/utils.js | 3 +- 10 files changed, 64 insertions(+), 83 deletions(-) create mode 100644 src/context.js diff --git a/README.md b/README.md index eaa5347..8f5d286 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ The first argument can be a function that returns a promise, a promise itself, o - `key`: `'foo'` || `module => module.foo` -- *default: `default` export in ES6 and `module.exports` in ES5* - `timeout`: `15000` -- *default* - `onError`: `(error, { isServer }) => handleError(error, isServer) -- `onLoad`: `(module, { isSync, isServer }, props, context) => do(module, isSync, isServer, props, context)` +- `onLoad`: `(module, { isSync, isServer }, props) => do(module, isSync, isServer, props)` - `minDelay`: `0` -- *default* - `alwaysDelay`: `false` -- *default* - `loadingTransition`: `true` -- *default* @@ -187,11 +187,11 @@ The first argument can be a function that returns a promise, a promise itself, o - `onLoad` is a callback function that receives the *entire* module. It allows you to export and put to use things other than your `default` component export, like reducers, sagas, etc. E.g: ```js - onLoad: (module, info, props, context) => { - context.store.replaceReducer({ ...otherReducers, foo: module.fooReducer }) + onLoad: (module, info, props) => { + props.store.replaceReducer({ ...otherReducers, foo: module.fooReducer }) // if a route triggered component change, new reducers needs to reflect it - context.store.dispatch({ type: 'INIT_ACTION_FOR_ROUTE', payload: { param: props.param } }) + props.store.dispatch({ type: 'INIT_ACTION_FOR_ROUTE', payload: { param: props.param } }) } ```` **As you can see we have thought of everything you might need to really do code-splitting right (we have real apps that use this stuff).** `onLoad` is fired directly before the component is rendered so you can setup any reducers/etc it depends on. Unlike the `onAfter` prop, this *option* to the `universal` *HOC* is only fired the first time the module is received. *Also note*: it will fire on the server, so do `if (!isServer)` if you have to. But also keep in mind you will need to do things like replace reducers on both the server + client for the imported component that uses new reducers to render identically in both places. diff --git a/__tests__/index.js b/__tests__/index.js index 590bb72..aa501e6 100644 --- a/__tests__/index.js +++ b/__tests__/index.js @@ -280,8 +280,7 @@ describe('other options', () => { await waitFor(50) const info = { isServer: false, isSync: false } const props = { foo: 'bar' } - const context = {} - expect(onLoad).toBeCalledWith(mod, info, props, context) + expect(onLoad).toBeCalledWith(mod, info, props) expect(component.toJSON()).toMatchSnapshot() // success }) @@ -299,8 +298,7 @@ describe('other options', () => { await waitFor(50) const info = { isServer: false, isSync: false } - const context = {} - expect(onLoad).toBeCalledWith(mod, info, {}, context) + expect(onLoad).toBeCalledWith(mod, info, {}) expect(component.toJSON()).toMatchSnapshot() // success }) @@ -322,7 +320,6 @@ describe('other options', () => { isServer: false, isSync: true }, - {}, {} ) }) diff --git a/__tests__/requireUniversalModule.js b/__tests__/requireUniversalModule.js index adb0d8b..eae6328 100644 --- a/__tests__/requireUniversalModule.js +++ b/__tests__/requireUniversalModule.js @@ -361,11 +361,10 @@ describe('other options', () => { }) const props = { foo: 'bar' } - const context = {} - await requireAsync(props, context) + await requireAsync(props) const info = { isServer: false, isSync: false } - expect(onLoad).toBeCalledWith(mod, info, props, context) + expect(onLoad).toBeCalledWith(mod, info, props) expect(onLoad).not.toBeCalledWith('foo', info, props) }) @@ -386,11 +385,10 @@ describe('other options', () => { }) const props = { foo: 'bar' } - const context = {} - requireSync(props, context) + requireSync(props) const info = { isServer: false, isSync: true } - expect(onLoad).toBeCalledWith(mod, info, props, context) + expect(onLoad).toBeCalledWith(mod, info, props) expect(onLoad).not.toBeCalledWith('foo', info, props) delete global.__webpack_require__ diff --git a/__tests__/utils.js b/__tests__/utils.js index 50d566e..80a8f47 100644 --- a/__tests__/utils.js +++ b/__tests__/utils.js @@ -57,13 +57,12 @@ test('resolveExport: finds export and calls onLoad', () => { const onLoad = jest.fn() const mod = { foo: 'bar' } const props = { baz: 123 } - const context = {} - const exp = resolveExport(mod, 'foo', onLoad, undefined, props, context) + const exp = resolveExport(mod, 'foo', onLoad, undefined, props) expect(exp).toEqual('bar') const info = { isServer: false, isSync: false } - expect(onLoad).toBeCalledWith(mod, info, props, context) + expect(onLoad).toBeCalledWith(mod, info, props) // todo: test caching }) diff --git a/src/context.js b/src/context.js new file mode 100644 index 0000000..969e389 --- /dev/null +++ b/src/context.js @@ -0,0 +1,5 @@ +import React from 'react' + +const ReportContext = React.createContext({ report: () => {} }) + +export default ReportContext diff --git a/src/flowTypes.js b/src/flowTypes.js index 99e863e..e3f836b 100644 --- a/src/flowTypes.js +++ b/src/flowTypes.js @@ -73,13 +73,12 @@ export type Key = string | null | ((module: ?(Object | Function)) => any) export type OnLoad = ( module: ?(Object | Function), info: { isServer: boolean }, - props: Object, - context: Object + props: Object ) => void export type OnError = (error: Object, info: { isServer: boolean }) => void -export type RequireAsync = (props: Object, context: Object) => Promise -export type RequireSync = (props: Object, context: Object) => ?any +export type RequireAsync = (props: Object) => Promise +export type RequireSync = (props: Object) => ?any export type AddModule = (props: Object) => ?string export type Mod = Object | Function export type Tools = { @@ -108,6 +107,10 @@ export type Props = { onError?: OnErrorProp } +export type Context = { + report?: (chunkName: string) => void +} + export type GenericComponent = Props => | React$Element | Class> diff --git a/src/index.js b/src/index.js index 3840235..0f38b73 100644 --- a/src/index.js +++ b/src/index.js @@ -9,8 +9,10 @@ import type { ComponentOptions, RequireAsync, State, - Props + Props, + Context } from './flowTypes' +import ReportContext from './context' import { DefaultLoading, @@ -63,16 +65,17 @@ export default function universal( state: State props: Props - context: Object /* eslint-enable react/sort-comp */ - static preload(props: Props, context: Object = {}) { + static contextType = ReportContext + + static preload(props: Props) { props = props || {} const { requireAsync, requireSync } = req(asyncModule, options, props) let mod try { - mod = requireSync(props, context) + mod = requireSync(props) } catch (error) { return Promise.reject(error) @@ -81,7 +84,7 @@ export default function universal( return Promise.resolve() .then(() => { if (mod) return mod - return requireAsync(props, context) + return requireAsync(props) }) .then(mod => { hoist(UniversalComponent, mod, { @@ -92,11 +95,11 @@ export default function universal( }) } - static preloadWeak(props: Props, context: Object = {}) { + static preloadWeak(props: Props) { props = props || {} const { requireSync } = req(asyncModule, options, props) - const mod = requireSync(props, context) + const mod = requireSync(props) if (mod) { hoist(UniversalComponent, mod, { preload: true, @@ -107,16 +110,10 @@ export default function universal( return mod } - static contextTypes = { - store: PropTypes.object, - report: PropTypes.func - } - requireAsyncInner( requireAsync: RequireAsync, props: Props, state: State, - context: Object = {}, isMount?: boolean ) { if (!state.mod && loadingTransition) { @@ -125,9 +122,9 @@ export default function universal( const time = new Date() - requireAsync(props, context) + requireAsync(props) .then((mod: ?any) => { - const state = { mod, props, context } + const state = { mod, props } const timeLapsed = new Date() - time if (timeLapsed < minDelay) { @@ -137,7 +134,7 @@ export default function universal( this.update(state, isMount) }) - .catch(error => this.update({ error, props, context })) + .catch(error => this.update({ error, props })) } update = ( @@ -191,7 +188,7 @@ export default function universal( this.setState(state) } // $FlowFixMe - init(props, context) { + init(props) { const { addModule, requireSync, requireAsync, asyncOnly } = req( asyncModule, options, @@ -201,24 +198,23 @@ export default function universal( let mod try { - mod = requireSync(props, context) + mod = requireSync(props) } catch (error) { - return __update(props, { error, props, context }, this._initialized) + return __update(props, { error, props }, this._initialized) } this._asyncOnly = asyncOnly const chunkName = addModule(props) // record the module for SSR flushing :) - - if (context.report) { - context.report(chunkName) + if (this.context && this.context.report) { + this.context.report(chunkName) } if (mod || isServer) { this.handleBefore(true, true, isServer) return __update( props, - { asyncOnly, props, mod, context }, + { asyncOnly, props, mod }, this._initialized, true, true, @@ -230,16 +226,15 @@ export default function universal( this.requireAsyncInner( requireAsync, props, - { props, asyncOnly, mod, context }, - context, + { props, asyncOnly, mod }, true ) - return { mod, asyncOnly, context, props } + return { mod, asyncOnly, props } } - constructor(props: Props, context: {}) { + constructor(props: Props, context: Context) { super(props, context) - this.state = this.init(this.props, this.context) + this.state = this.init(this.props) // $FlowFixMe this.state.error = null } @@ -252,7 +247,7 @@ export default function universal( currentState.props ) if (isHMR() && shouldUpdate(currentState.props, nextProps)) { - const mod = requireSync(nextProps, currentState.context) + const mod = requireSync(nextProps) return { ...currentState, mod } } return null @@ -275,7 +270,7 @@ export default function universal( let mod try { - mod = requireSync(this.props, this.context) + mod = requireSync(this.props) } catch (error) { return this.update({ error }) diff --git a/src/report-chunks.js b/src/report-chunks.js index 66dfda1..615d67e 100644 --- a/src/report-chunks.js +++ b/src/report-chunks.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' +import ReportContext from './context' type Props = { report: Function, @@ -13,17 +14,18 @@ export default class ReportChunks extends React.Component { report: PropTypes.func.isRequired } - static childContextTypes = { - report: PropTypes.func.isRequired - } - - getChildContext() { - return { - report: this.props.report + constructor(props: Props) { + super(props) + this.state = { + report: props.report } } render() { - return React.Children.only(this.props.children) + return ( + + {this.props.children} + + ) } } diff --git a/src/requireUniversalModule.js b/src/requireUniversalModule.js index ffff18c..a2618b0 100644 --- a/src/requireUniversalModule.js +++ b/src/requireUniversalModule.js @@ -48,7 +48,7 @@ export default function requireUniversalModule( const { chunkName, path, resolve, load } = config const asyncOnly = (!path && !resolve) || typeof chunkName === 'function' - const requireSync = (props: Object, context: Object): ?any => { + const requireSync = (props: Object): ?any => { let exp = loadFromCache(chunkName, props, modCache) if (!exp) { @@ -67,23 +67,14 @@ export default function requireUniversalModule( } if (mod) { - exp = resolveExport( - mod, - key, - onLoad, - chunkName, - props, - context, - modCache, - true - ) + exp = resolveExport(mod, key, onLoad, chunkName, props, modCache, true) } } return exp } - const requireAsync = (props: Object, context: Object): Promise => { + const requireAsync = (props: Object): Promise => { const exp = loadFromCache(chunkName, props, modCache) if (exp) return Promise.resolve(exp) @@ -108,15 +99,7 @@ export default function requireUniversalModule( const resolve = mod => { clearTimeout(timer) - const exp = resolveExport( - mod, - key, - onLoad, - chunkName, - props, - context, - modCache - ) + const exp = resolveExport(mod, key, onLoad, chunkName, props, modCache) if (exp) return res(exp) reject(new Error('export not found')) diff --git a/src/utils.js b/src/utils.js index a9bfe3c..7f1cb9e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -55,7 +55,6 @@ export const resolveExport = ( onLoad: ?OnLoad, chunkName: ?StrFun, props: Object, - context: Object, modCache: Object, isSync?: boolean = false ) => { @@ -63,7 +62,7 @@ export const resolveExport = ( if (onLoad && mod) { const isServer = typeof window === 'undefined' const info = { isServer, isSync } - onLoad(mod, info, props, context) + onLoad(mod, info, props) } if (chunkName && exp) cacheExport(exp, chunkName, props, modCache) return exp