Skip to content

Commit

Permalink
Merge branch 'master' of github.com:smooth-code/loadable-components i…
Browse files Browse the repository at this point in the history
…nto master
  • Loading branch information
theKashey committed Sep 11, 2020
2 parents b86bb82 + c9d1663 commit 49e81dd
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 88 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"jest": true
},
"rules": {
"react/jsx-one-expression-per-line": "off",
"no-plusplus": "off",
"no-param-reassign": "off",
"no-nested-ternary": "off",
Expand Down
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ package.json
lerna.json
/website/.cache/
/website/public/
__testfixtures__/
__testfixtures__/
examples/*/public
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ _Before_ submitting a pull request, please make sure the following is done…
yarn --version
```

4. Run `yarn build` to bootstrap packages.
4. Run `yarn build` to bootstrap packages.

```sh
yarn build
Expand Down
5 changes: 0 additions & 5 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,4 @@ module.exports = {
['@babel/preset-env', { loose: true, targets: getTargets() }],
],
plugins: ['@babel/plugin-proposal-class-properties'],
env: {
test: {
plugins: ['@babel/plugin-proposal-function-bind'],
},
},
}
8 changes: 1 addition & 7 deletions examples/server-side-rendering/src/client/letters/G.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import React from 'react'

const G = ({prefix}) => (
<span className="my-cool-class">
{prefix}
{' '}
- G
</span>
)
const G = ({ prefix }) => <span className="my-cool-class">{prefix} - G</span>

export default G
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"@babel/core": "^7.7.7",
"@babel/node": "^7.7.7",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-proposal-function-bind": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.7",
"@babel/preset-react": "^7.7.4",
Expand Down
169 changes: 113 additions & 56 deletions packages/component/src/createLoadable.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ const STATUS_REJECTED = 'REJECTED'

function resolveConstructor(ctor) {
if (typeof ctor === 'function') {
return { requireAsync: ctor }
return {
requireAsync: ctor,
resolve() {
return undefined
},
chunkName() {
return undefined
},
}
}

return ctor
Expand All @@ -35,6 +43,12 @@ function createLoadable({
const ctor = resolveConstructor(loadableConstructor)
const cache = {}

/**
* Cachekey represents the component to be loaded
* if key changes - component has to be reloaded
* @param props
* @returns {null|Component}
*/
function getCacheKey(props) {
if (options.cacheKey) {
return options.cacheKey(props)
Expand All @@ -45,10 +59,18 @@ function createLoadable({
return null
}

/**
* Resolves loaded `module` to a specific `Component
* @param module
* @param props
* @param Loadable
* @returns Component
*/
function resolve(module, props, Loadable) {
const Component = options.resolveComponent
? options.resolveComponent(module, props)
: defaultResolveComponent(module)

if (options.resolveComponent && !ReactIs.isValidElementType(Component)) {
throw new Error(
`resolveComponent returned something that is not a React component!`,
Expand All @@ -66,6 +88,7 @@ function createLoadable({
return {
...state,
cacheKey,
// change of a key triggers loading state automatically
loading: state.loading || state.cacheKey !== cacheKey,
}
}
Expand Down Expand Up @@ -94,7 +117,7 @@ function createLoadable({

// We run load function, we assume that it won't fail and that it
// triggers a synchronous loading of the module
ctor.requireAsync(props).catch(() => {})
ctor.requireAsync(props).catch(() => null)

// So we can require now the module synchronously
this.loadSync()
Expand Down Expand Up @@ -122,22 +145,24 @@ function createLoadable({
componentDidMount() {
this.mounted = true

// retrieve loading promise from a global cache
const cachedPromise = this.getCache()

// if promise exists, but rejected - clear cache
if (cachedPromise && cachedPromise.status === STATUS_REJECTED) {
this.setCache()
this.setState({
error: undefined,
loading: true,
})
}
this.loadAsyncOnLifecycle()

// component might be resolved synchronously in the constructor
if (this.state.loading) {
this.loadAsync()
}
}

componentDidUpdate(prevProps, prevState) {
// Component is reloaded if the cacheKey has changed
// Component has to be reloaded on cacheKey change
if (prevState.cacheKey !== this.state.cacheKey) {
this.loadAsyncOnLifecycle()
this.loadAsync()
}
}

Expand All @@ -151,6 +176,28 @@ function createLoadable({
}
}

/**
* returns a cache key for the current props
* @returns {Component|string}
*/
getCacheKey() {
return getCacheKey(this.props) || JSON.stringify(this.props)
}

/**
* access the persistent cache
*/
getCache() {
return cache[this.getCacheKey()]
}

/**
* sets the cache value. If called without value sets it as undefined
*/
setCache(value = undefined) {
cache[this.getCacheKey()] = value
}

triggerOnLoad() {
if (onLoad) {
setTimeout(() => {
Expand All @@ -159,7 +206,14 @@ function createLoadable({
}
}

/**
* Synchronously loads component
* target module is expected to already exists in the module cache
* or be capable to resolve synchronously (webpack target=node)
*/
loadSync() {
// load sync is expecting component to be in the "loading" state already
// sounds weird, but loading=true is the initial state of InnerLoadable
if (!this.state.loading) return

try {
Expand All @@ -168,28 +222,45 @@ function createLoadable({
this.state.result = result
this.state.loading = false
} catch (error) {
console.error('loadable-components: failed to synchronously load component, which expected to be available', {
fileName: ctor.resolve(this.props),
chunkName: ctor.chunkName(this.props),
error: error.message,
});
console.error(
'loadable-components: failed to synchronously load component, which expected to be available',
{
fileName: ctor.resolve(this.props),
chunkName: ctor.chunkName(this.props),
error: error ? error.message : error,
},
)
this.state.error = error
}
}

getCacheKey() {
return getCacheKey(this.props) || JSON.stringify(this.props)
}
/**
* Asynchronously loads a component.
*/
loadAsync() {
const promise = this.resolveAsync()

getCache() {
return cache[this.getCacheKey()]
}
promise
.then(loadedModule => {
const result = resolve(loadedModule, this.props, { Loadable })
this.safeSetState(
{
result,
loading: false,
},
() => this.triggerOnLoad(),
)
})
.catch(error => this.safeSetState({ error, loading: false }))

setCache(value) {
cache[this.getCacheKey()] = value
return promise
}

loadAsync() {
/**
* Asynchronously resolves(not loads) a component.
* Note - this function does not change the state
*/
resolveAsync() {
const { __chunkExtractor, forwardedRef, ...props } = this.props

let promise = this.getCache()
Expand All @@ -200,42 +271,27 @@ function createLoadable({

this.setCache(promise)

const cachedPromise = promise

promise = promise
.then(loadedModule => {
cachedPromise.status = STATUS_RESOLVED
return loadedModule
})
.catch(error => {
console.error('loadable-components: failed to asynchronously load component', {
fileName: ctor.resolve(this.props),
chunkName: ctor.chunkName(this.props),
error: error.message,
});
cachedPromise.status = STATUS_REJECTED
throw error
})
promise.then(
() => {
promise.status = STATUS_RESOLVED
},
error => {
console.error(
'loadable-components: failed to asynchronously load component',
{
fileName: ctor.resolve(this.props),
chunkName: ctor.chunkName(this.props),
error: error ? error.message : error,
},
)
promise.status = STATUS_REJECTED
},
)
}

return promise
}

loadAsyncOnLifecycle() {
this.loadAsync()
.then(loadedModule => {
const result = resolve(loadedModule, this.props, { Loadable })
this.safeSetState(
{
result,
loading: false,
},
() => this.triggerOnLoad(),
)
})
.catch(error => this.safeSetState({ error, loading: false }))
}

render() {
const {
forwardedRef,
Expand All @@ -246,8 +302,8 @@ function createLoadable({
const { error, loading, result } = this.state

if (options.suspense) {
const cachedPromise = this.getCache()
if (!cachedPromise || cachedPromise.status === STATUS_PENDING) {
const cachedPromise = this.getCache() || this.loadAsync()
if (cachedPromise.status === STATUS_PENDING) {
throw this.loadAsync()
}
}
Expand All @@ -263,6 +319,7 @@ function createLoadable({
}

return render({
fallback,
result,
options,
props: { ...props, ref: forwardedRef },
Expand Down
Loading

0 comments on commit 49e81dd

Please sign in to comment.