Skip to content

Commit

Permalink
feat: Upgrade React Context API (#216)
Browse files Browse the repository at this point in the history
  • Loading branch information
ithinkdancan committed Nov 1, 2020
1 parent db7ea3d commit 29a360e
Show file tree
Hide file tree
Showing 10 changed files with 64 additions and 83 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 2 additions & 5 deletions __tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand All @@ -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
})
Expand All @@ -322,7 +320,6 @@ describe('other options', () => {
isServer: false,
isSync: true
},
{},
{}
)
})
Expand Down
10 changes: 4 additions & 6 deletions __tests__/requireUniversalModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand All @@ -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__
Expand Down
5 changes: 2 additions & 3 deletions __tests__/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})

Expand Down
5 changes: 5 additions & 0 deletions src/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react'

const ReportContext = React.createContext({ report: () => {} })

export default ReportContext
11 changes: 7 additions & 4 deletions src/flowTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<?any>
export type RequireSync = (props: Object, context: Object) => ?any
export type RequireAsync = (props: Object) => Promise<?any>
export type RequireSync = (props: Object) => ?any
export type AddModule = (props: Object) => ?string
export type Mod = Object | Function
export type Tools = {
Expand Down Expand Up @@ -108,6 +107,10 @@ export type Props = {
onError?: OnErrorProp
}

export type Context = {
report?: (chunkName: string) => void
}

export type GenericComponent<Props> = Props =>
| React$Element<any>
| Class<React.Component<{}, Props, mixed>>
Expand Down
55 changes: 25 additions & 30 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import type {
ComponentOptions,
RequireAsync,
State,
Props
Props,
Context
} from './flowTypes'
import ReportContext from './context'

import {
DefaultLoading,
Expand Down Expand Up @@ -63,16 +65,17 @@ export default function universal<Props: Props>(

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)
Expand All @@ -81,7 +84,7 @@ export default function universal<Props: Props>(
return Promise.resolve()
.then(() => {
if (mod) return mod
return requireAsync(props, context)
return requireAsync(props)
})
.then(mod => {
hoist(UniversalComponent, mod, {
Expand All @@ -92,11 +95,11 @@ export default function universal<Props: Props>(
})
}

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,
Expand All @@ -107,16 +110,10 @@ export default function universal<Props: Props>(
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) {
Expand All @@ -125,9 +122,9 @@ export default function universal<Props: Props>(

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) {
Expand All @@ -137,7 +134,7 @@ export default function universal<Props: Props>(

this.update(state, isMount)
})
.catch(error => this.update({ error, props, context }))
.catch(error => this.update({ error, props }))
}

update = (
Expand Down Expand Up @@ -191,7 +188,7 @@ export default function universal<Props: Props>(
this.setState(state)
}
// $FlowFixMe
init(props, context) {
init(props) {
const { addModule, requireSync, requireAsync, asyncOnly } = req(
asyncModule,
options,
Expand All @@ -201,24 +198,23 @@ export default function universal<Props: Props>(
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,
Expand All @@ -230,16 +226,15 @@ export default function universal<Props: Props>(
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
}
Expand All @@ -252,7 +247,7 @@ export default function universal<Props: Props>(
currentState.props
)
if (isHMR() && shouldUpdate(currentState.props, nextProps)) {
const mod = requireSync(nextProps, currentState.context)
const mod = requireSync(nextProps)
return { ...currentState, mod }
}
return null
Expand All @@ -275,7 +270,7 @@ export default function universal<Props: Props>(
let mod

try {
mod = requireSync(this.props, this.context)
mod = requireSync(this.props)
}
catch (error) {
return this.update({ error })
Expand Down
18 changes: 10 additions & 8 deletions src/report-chunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import React from 'react'
import PropTypes from 'prop-types'
import ReportContext from './context'

type Props = {
report: Function,
Expand All @@ -13,17 +14,18 @@ export default class ReportChunks extends React.Component<void, Props, *> {
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 (
<ReportContext.Provider value={this.state}>
{this.props.children}
</ReportContext.Provider>
)
}
}
25 changes: 4 additions & 21 deletions src/requireUniversalModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default function requireUniversalModule<Props: Props>(
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) {
Expand All @@ -67,23 +67,14 @@ export default function requireUniversalModule<Props: Props>(
}

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<?any> => {
const requireAsync = (props: Object): Promise<?any> => {
const exp = loadFromCache(chunkName, props, modCache)
if (exp) return Promise.resolve(exp)

Expand All @@ -108,15 +99,7 @@ export default function requireUniversalModule<Props: Props>(
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'))
Expand Down
Loading

0 comments on commit 29a360e

Please sign in to comment.