Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Upgrade React Context API #216

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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