-
Notifications
You must be signed in to change notification settings - Fork 2
/
react-loadable-client.js
161 lines (138 loc) · 4.31 KB
/
react-loadable-client.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/* global Meteor */
import { EJSON } from 'meteor/ejson'
import { useState, useEffect, useReducer, createElement } from 'react'
import { load, loadMap, resolveRender, flushInitializers } from './react-loadable-both'
const INITIALIZERS = []
const INITIALIZERS_BY_MODULE = {}
// Used to create a forceUpdate from useReducer. Forces update by
// incrementing a number whenever the dispatch method is invoked.
const fur = x => x + 1
/**
* Creates a "Loadable" component at startup, which will persist for the length of the program.
*/
const createLoadable = (load) => ({ render = resolveRender, meteor, loader, loading, delay = 200, timeout = null }) => {
if (!loading) {
throw new Error('react-loadable requires a `loading` component')
}
// Gets ready to load the module, and maintain status
let status = null
function init () {
if (!status) {
status = load(loader)
}
return status.promise
}
// Store all the INITIALIZERS for later use
INITIALIZERS.push(init)
if (typeof meteor === 'function') {
INITIALIZERS_BY_MODULE[meteor().sort().join(',')] = () => {
return init()
}
}
function Loadable (props) {
// We are starting load as early as possible. We are counting
// on Meteor to avoid problems if this happens to get fired
// off more than once in concurrent mode.
if (!status) {
init()
}
const [pastDelay, setPastDelay] = useState(delay === 0)
const [timedOut, setTimedOut] = useState(false)
const [, forceUpdate] = useReducer(fur, 0)
const wasLoading = status.loading
useEffect(() => {
// If status.loading is false, then we either have an error
// state, or have loaded successfully. In either case, we
// don't need to set up any timeouts, or watch for updates.
if (!status.loading) {
// It's possible loading completed between render and commit.
if (wasLoading) {
forceUpdate()
}
return
}
// If we got this far, we need to set up the two timeouts
let tidDelay
let tidTimeout
const _clearTimeouts = () => {
if (tidDelay) clearTimeout(tidDelay)
if (tidTimeout) clearTimeout(tidTimeout)
}
if (typeof delay === 'number' && delay > 0) {
tidDelay = setTimeout(() => {
setPastDelay(true)
}, delay)
}
if (typeof timeout === 'number') {
tidTimeout = setTimeout(() => {
setTimedOut(true)
}, timeout)
}
// Use to avoid updating state after unmount.
let mounted = true
const update = () => {
_clearTimeouts()
if (mounted) {
forceUpdate()
}
}
status.promise
.then(() => {
update()
})
.catch(err => {
console.error(err)
update()
})
return () => {
mounted = false
_clearTimeouts()
}
}, [])
// render
if (status.loading || status.error) {
return createElement(loading, {
isLoading: status.loading,
pastDelay: pastDelay,
timedOut: timedOut,
error: status.error
})
} else if (status.loaded) {
return render(status.loaded, props)
} else {
return null
}
}
Loadable.preload = () => {
return init()
}
return Loadable
}
export const Loadable = createLoadable(load)
export const LoadableMap = Meteor.isDevelopment
? (((LoadableMap) => (opts) => {
if (typeof opts.render !== 'function') {
throw new Error('LoadableMap requires a `render(loaded, props)` function')
}
return LoadableMap(opts)
})(createLoadable(loadMap)))
: createLoadable(loadMap)
// For backward compat and easy porting
Loadable.Map = LoadableMap
const preload = (preloadables) => {
const initializers = preloadables.map(preloadable => {
return INITIALIZERS_BY_MODULE[preloadable]
})
return flushInitializers(initializers)
}
export const preloadLoadables = (id = '__preloadables__') => {
const preloadablesNode = document.getElementById(id)
if (preloadablesNode) {
const preloadables = EJSON.parse(preloadablesNode.innerText)
preloadablesNode.parentNode.removeChild(preloadablesNode)
return preload(preloadables)
} else {
return new Promise((resolve) => { resolve() })
}
}
export const preloadAllLoadables = () => flushInitializers(INITIALIZERS)