├── .eslintignore ├── .npmignore ├── .travis.yml ├── .babelrc ├── tests.webpack.js ├── .gitignore ├── LICENSE ├── package.json ├── karma.conf.js ├── test ├── index.spec.js └── utils.spec.js ├── src ├── utils.js └── index.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'iojs' 4 | - '0.12' 5 | - '0.10' 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "stage-0", "es2015", "react" ], 3 | "plugins": [ "transform-class-properties" ] 4 | } 5 | -------------------------------------------------------------------------------- /tests.webpack.js: -------------------------------------------------------------------------------- 1 | // require all modules ending in ".spec.js" from the 2 | // current directory and all subdirectories 3 | var testsContext = require.context("./test", true, /\.spec\.js$/) 4 | testsContext.keys().forEach(testsContext) 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | lib 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 高振东 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-async-script-loader", 3 | "version": "0.2.3", 4 | "description": "A decorator for script lazy loading on react component", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir lib", 8 | "prepublish": "npm run build", 9 | "test": "karma start" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/leozdgao/react-script-loader.git" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "reactjs", 18 | "react-component" 19 | ], 20 | "author": "leozdgao", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/leozdgao/react-script-loader/issues" 24 | }, 25 | "homepage": "https://github.com/leozdgao/react-script-loader#readme", 26 | "peerDependencies": { 27 | "react": "^15.0.1 || ^16.0.0" 28 | }, 29 | "devDependencies": { 30 | "babel-cli": "^6.1.18", 31 | "babel-eslint": "^4.1.5", 32 | "babel-loader": "~6.2.4", 33 | "babel-plugin-syntax-class-properties": "^6.1.18", 34 | "babel-plugin-transform-class-properties": "^6.1.20", 35 | "babel-preset-es2015": "^6.1.18", 36 | "babel-preset-react": "^6.1.18", 37 | "babel-preset-stage-0": "^6.1.18", 38 | "chai": "~3.5.0", 39 | "eslint": "^1.9.0", 40 | "karma": "~0.13.22", 41 | "karma-chai": "~0.1.0", 42 | "karma-mocha": "~0.2.2", 43 | "karma-mocha-reporter": "~2.0.0", 44 | "karma-phantomjs-launcher": "~1.0.0", 45 | "karma-sourcemap-loader": "~0.3.7", 46 | "karma-webpack": "~1.7.0", 47 | "mocha": "~2.4.5", 48 | "phantomjs-prebuilt": "~2.1.7", 49 | "prop-types": "~15.5.8", 50 | "react": "^15.0.1", 51 | "react-addons-test-utils": "^15.0.1", 52 | "react-dom": "^15.0.1", 53 | "webpack": "~1.12.14" 54 | }, 55 | "dependencies": { 56 | "hoist-non-react-statics": "^1.0.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | 3 | const webpack = require('webpack') 4 | 5 | module.exports = function (config) { 6 | config.set({ 7 | 8 | // base path that will be used to resolve all patterns (eg. files, exclude) 9 | basePath: './', 10 | 11 | 12 | // frameworks to use 13 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 14 | frameworks: [ 'mocha', 'chai' ], 15 | 16 | 17 | // list of files / patterns to load in the browser 18 | files: [ 19 | 'tests.webpack.js' 20 | ], 21 | 22 | // list of files to exclude 23 | exclude: [ 24 | ], 25 | 26 | 27 | // preprocess matching files before serving them to the browser 28 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 29 | preprocessors: { 30 | 'tests.webpack.js': [ 'webpack', 'sourcemap' ] 31 | }, 32 | 33 | webpack: { 34 | devtool: 'inline-source-map', 35 | module: { 36 | loaders: [ 37 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel' } 38 | ] 39 | }, 40 | plugins: [ 41 | new webpack.DefinePlugin({ 42 | 'process.env.NODE_ENV': JSON.stringify('test') 43 | }) 44 | ] 45 | }, 46 | 47 | webpackMiddleware: { 48 | noInfo: true 49 | }, 50 | 51 | // test results reporter to use 52 | // possible values: 'dots', 'progress' 53 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 54 | reporters: [ 'mocha' ], 55 | 56 | // web server port 57 | port: 9876, 58 | 59 | 60 | // enable / disable colors in the output (reporters and logs) 61 | colors: true, 62 | 63 | 64 | // level of logging 65 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 66 | logLevel: config.LOG_INFO, 67 | 68 | 69 | // enable / disable watching file and executing tests whenever any file changes 70 | autoWatch: false, 71 | 72 | 73 | // start these browsers 74 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 75 | browsers: [ 'PhantomJS' ], 76 | 77 | 78 | // Continuous Integration mode 79 | // if true, Karma captures browsers, runs the tests and exits 80 | singleRun: true, 81 | 82 | // Concurrency level 83 | // how many browser should be started simultaneous 84 | concurrency: Infinity 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | const ReactTestUtils = require('react-dom/test-utils') 2 | const React = require('react') 3 | const AsyncScriptLoader = require('../src').default 4 | 5 | class TestComponent extends React.Component { 6 | render () { 7 | return
8 | } 9 | } 10 | 11 | function renderTestComponent (deps, onScriptLoaded) { 12 | const MockedComponent = AsyncScriptLoader.apply(null, deps)(TestComponent) 13 | const result = ReactTestUtils.renderIntoDocument() 14 | 15 | return ReactTestUtils.findRenderedComponentWithType(result, TestComponent) 16 | } 17 | 18 | function checkScriptLoaded (getComponent, done) { 19 | return _ => { 20 | const com = getComponent() 21 | 22 | expect(com.props.isScriptLoaded).to.be.true 23 | expect(com.props.isScriptLoadSucceed).to.be.true 24 | 25 | done() 26 | } 27 | } 28 | 29 | describe('Test this module', _ => { 30 | it('[react-async-script-loader] Load external script after component mounted', 31 | function (done) { 32 | const deps = [ 'https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js' ] 33 | const com = renderTestComponent(deps, onScriptLoaded) 34 | 35 | this.timeout(5000) 36 | 37 | // check script tags 38 | deps.forEach(testScript => { 39 | const tag = document.querySelector(`script[src='${testScript}']`) 40 | expect(tag).to.be.exist 41 | }) 42 | 43 | // check component props before loading 44 | expect(com.props.isScriptLoaded).to.be.false 45 | expect(com.props.isScriptLoadSucceed).to.be.false 46 | 47 | function onScriptLoaded () { 48 | expect(com.props.isScriptLoaded).to.be.true 49 | expect(com.props.isScriptLoadSucceed).to.be.true 50 | 51 | done() 52 | } 53 | } 54 | ) 55 | 56 | it('[react-async-script-loader] No redundant script tag will be appended', 57 | function (done) { 58 | const deps = [ '//cdn.bootcss.com/jquery/2.1.1/jquery.min.js' ] 59 | const com0 = renderTestComponent(deps, checkScriptLoaded(_ => com0, checkAllDone)) 60 | const com1 = renderTestComponent(deps, checkScriptLoaded(_ => com1, checkAllDone)) 61 | let count = 0 62 | 63 | this.timeout(5000) 64 | 65 | // check script tags 66 | deps.forEach(testScript => { 67 | const tags = document.querySelectorAll(`script[src='${testScript}']`) 68 | expect(tags.length).to.equal(1) 69 | }) 70 | 71 | function checkAllDone () { 72 | count ++ 73 | if (count == 2) done() 74 | } 75 | } 76 | ) 77 | }) 78 | -------------------------------------------------------------------------------- /test/utils.spec.js: -------------------------------------------------------------------------------- 1 | const { newScript, parallel, series } = require('../src/utils') 2 | 3 | describe('Test util functions', _ => { 4 | const testTask = v => cb => setTimeout(_ => cb(null, v), 100) 5 | const taskBundle = [1, 2, 3, 4, 5].map(testTask) 6 | 7 | it('[utils/newScript] A thunk task, append new script tag', function (done) { 8 | const testScript = 'https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js' 9 | const task = newScript(testScript) 10 | 11 | // start task 12 | task((err, src) => { 13 | const tag = document.querySelector(`script[src='${testScript}']`) 14 | 15 | // assert 16 | expect(err).to.not.exist 17 | expect(src).to.equal(testScript) 18 | expect(tag).to.exist 19 | 20 | done() 21 | }) 22 | }) 23 | it('[utils/newScript] A thunk task, append new script tag with id', function (done) { 24 | const testScript = 'https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js' 25 | const task = newScript({src: testScript, id: 'test'}) 26 | 27 | // start task 28 | task((err, src) => { 29 | const tag = document.querySelector(`script[src='${testScript}']`) 30 | 31 | // assert 32 | expect(err).to.not.exist 33 | expect(src).to.equal(testScript) 34 | expect(tag).to.exist 35 | 36 | done() 37 | }) 38 | }) 39 | 40 | it('[utils/parallel] Run thunk task in parallel mode', function (done) { 41 | const startTime = Date.now() 42 | 43 | parallel.apply(null, taskBundle)((err, val, i) => { 44 | // assert for iteration 45 | expect(err).to.not.exist 46 | expect(val).to.equal(i + 1) 47 | })((err, ret) => { 48 | const finishTime = Date.now() 49 | const delta = finishTime - startTime 50 | 51 | // assert for success callback 52 | expect(err).to.not.exist 53 | expect(ret).to.eql([1, 2, 3, 4, 5]) 54 | // check execute time, parallel mode would take about >100, <200 ms 55 | expect(delta).to.be.below(200) 56 | 57 | done() 58 | }) 59 | }) 60 | 61 | it('[utils/series] Run thunk task in series mode', function (done) { 62 | const startTime = Date.now() 63 | 64 | series.apply(null, taskBundle)((err, val, i) => { 65 | // assert for iteration 66 | expect(err).to.not.exist 67 | expect(val).to.equal(i + 1) 68 | })((err, ret) => { 69 | const finishTime = Date.now() 70 | const delta = finishTime - startTime 71 | 72 | // assert for success callback 73 | expect(err).to.not.exist 74 | expect(ret).to.eql([1, 2, 3, 4, 5]) 75 | // check execute time, series mode would >500 ms 76 | expect(delta).to.be.above(500) 77 | 78 | done() 79 | }) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const isDefined = val => val != null 2 | export const isFunction = val => typeof val === 'function' 3 | export const noop = _ => { } 4 | 5 | export const newScript = (src) => (cb) => { 6 | const scriptElem = document.createElement('script') 7 | if (typeof src === 'object') { 8 | // copy every property to the element 9 | for (var key in src) { 10 | if (Object.prototype.hasOwnProperty.call(src, key)) { 11 | scriptElem[key] = src[key]; 12 | } 13 | } 14 | src = src.src; 15 | } else { 16 | scriptElem.src = src 17 | } 18 | scriptElem.addEventListener('load', () => cb(null, src)) 19 | scriptElem.addEventListener('error', () => cb(true, src)) 20 | document.body.appendChild(scriptElem) 21 | return scriptElem 22 | } 23 | 24 | const keyIterator = (cols) => { 25 | const keys = Object.keys(cols) 26 | let i = -1 27 | return { 28 | next () { 29 | i++ // inc 30 | if (i >= keys.length) return null 31 | else return keys[i] 32 | } 33 | } 34 | } 35 | 36 | // tasks should be a collection of thunk 37 | export const parallel = (...tasks) => (each) => (cb) => { 38 | let hasError = false 39 | let successed = 0 40 | const ret = [] 41 | tasks = tasks.filter(isFunction) 42 | 43 | if (tasks.length <= 0) cb(null) 44 | else { 45 | tasks.forEach((task, i) => { 46 | const thunk = task 47 | thunk((err, ...args) => { 48 | if (err) hasError = true 49 | else { 50 | // collect result 51 | if (args.length <= 1) args = args[0] 52 | 53 | ret[i] = args 54 | successed ++ 55 | } 56 | 57 | if (isFunction(each)) each.call(null, err, args, i) 58 | 59 | if (hasError) cb(true) 60 | else if (tasks.length === successed) { 61 | cb(null, ret) 62 | } 63 | }) 64 | }) 65 | } 66 | } 67 | 68 | // tasks should be a collection of thunk 69 | export const series = (...tasks) => (each) => (cb) => { 70 | tasks = tasks.filter(val => val != null) 71 | const nextKey = keyIterator(tasks) 72 | const nextThunk = () => { 73 | const key = nextKey.next() 74 | let thunk = tasks[key] 75 | if (Array.isArray(thunk)) thunk = parallel.apply(null, thunk).call(null, each) 76 | return [ +key, thunk ] // convert `key` to number 77 | } 78 | let key, thunk 79 | let next = nextThunk() 80 | key = next[0] 81 | thunk = next[1] 82 | if (thunk == null) return cb(null) 83 | 84 | const ret = [] 85 | const iterator = () => { 86 | thunk((err, ...args) => { 87 | if (args.length <= 1) args = args[0] 88 | if (isFunction(each)) each.call(null, err, args, key) 89 | 90 | if (err) cb(err) 91 | else { 92 | // collect result 93 | ret.push(args) 94 | 95 | next = nextThunk() 96 | key = next[0] 97 | thunk = next[1] 98 | if (thunk == null) return cb(null, ret) // finished 99 | else iterator() 100 | } 101 | }) 102 | } 103 | iterator() 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-async-script-loader 2 | 3 | [![Build Status](https://travis-ci.org/leozdgao/react-async-script-loader.svg?branch=master)](https://travis-ci.org/leozdgao/react-async-script-loader) [![npm version](https://badge.fury.io/js/react-async-script-loader.svg)](https://badge.fury.io/js/react-async-script-loader) 4 | 5 | A decorator for script lazy loading on react component. 6 | 7 | ## Description 8 | 9 | Some component may depend on other vendors which you may not want to load them until you really need them. So here it is, use **High Order Component** to decorate your component and it will handle lazy loading for you, it support parallel and sequential loading. 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm install --save react-async-script-loader 15 | ``` 16 | 17 | ## API 18 | 19 | ```javascript 20 | scriptLoader(...scriptSrc)([WrappedComponent]) 21 | ``` 22 | 23 | `scriptSrc` can be a string of source or an array of source. `scriptSrc` will be loaded sequentially, but array of source will be loaded parallelly. It also cache the loaded script to avoid duplicated loading. More lively description see use case below. 24 | 25 | ## Properties 26 | 27 | Decorated component will receive following properties: 28 | 29 | |Name|Type|Description| 30 | |----|----|-----------| 31 | |isScriptLoaded|Boolean|Represent scripts loading process is over or not, maybe part of scripts load failed.| 32 | |isScriptLoadSucceed|Boolean|Represent all scripts load successfully or not.| 33 | |onScriptLoaded|Function|Triggered when all scripts load successfully.| 34 | 35 | ## How to use 36 | 37 | You can use it to decorate your component. 38 | 39 | ```javascript 40 | import React, { Component } from 'react' 41 | import scriptLoader from 'react-async-script-loader' 42 | 43 | class Editor extends Component { 44 | ... 45 | 46 | componentWillReceiveProps ({ isScriptLoaded, isScriptLoadSucceed }) { 47 | if (isScriptLoaded && !this.props.isScriptLoaded) { // load finished 48 | if (isScriptLoadSucceed) { 49 | this.initEditor() 50 | } 51 | else this.props.onError() 52 | } 53 | } 54 | 55 | componentDidMount () { 56 | const { isScriptLoaded, isScriptLoadSucceed } = this.props 57 | if (isScriptLoaded && isScriptLoadSucceed) { 58 | this.initEditor() 59 | } 60 | } 61 | 62 | ... 63 | } 64 | 65 | export default scriptLoader( 66 | [ 67 | 'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js', 68 | 'https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js' 69 | ], 70 | '/assets/bootstrap-markdown.js' 71 | )(Editor) 72 | ``` 73 | 74 | The example above means that the `jquery` and `marked` will be loading parallelly, and after loaded these 2 vendors, load `bootstrap-markdown` sequentially. 75 | 76 | It is possible that some script will be failed to load. ScriptLoader will cache the script that load successfully and will remove the script node which fail to load before. 77 | 78 | *Currently, if you try to reload scripts, you have to remount your component.* 79 | 80 | And it's cooler if you use decorator syntax. (ES7) 81 | 82 | ```javascript 83 | @scriptLoader( 84 | [ 85 | 'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js', 86 | 'https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js' 87 | ], 88 | '/assets/bootstrap-markdown.js' 89 | ) 90 | class Editor extends Component { 91 | 92 | } 93 | ``` 94 | 95 | ## license 96 | 97 | MIT 98 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import hoistStatics from 'hoist-non-react-statics' 4 | import { newScript, series, noop } from './utils' 5 | 6 | const loadedScript = [] 7 | const pendingScripts = {} 8 | let failedScript = [] 9 | 10 | export function startLoadingScripts(scripts, onComplete = noop) { 11 | // sequence load 12 | const loadNewScript = (script) => { 13 | const src = typeof script === 'object' ? script.src : script 14 | if (loadedScript.indexOf(src) < 0) { 15 | return taskComplete => { 16 | const callbacks = pendingScripts[src] || [] 17 | callbacks.push(taskComplete) 18 | pendingScripts[src] = callbacks 19 | if (callbacks.length === 1) { 20 | return newScript(script)(err => { 21 | pendingScripts[src].forEach(cb => cb(err, src)) 22 | delete pendingScripts[src] 23 | }) 24 | } 25 | } 26 | } 27 | } 28 | const tasks = scripts.map(src => { 29 | if (Array.isArray(src)) { 30 | return src.map(loadNewScript) 31 | } 32 | else return loadNewScript(src) 33 | }) 34 | 35 | series(...tasks)((err, src) => { 36 | if (err) { 37 | failedScript.push(src) 38 | } 39 | else { 40 | if (Array.isArray(src)) { 41 | src.forEach(addCache) 42 | } 43 | else addCache(src) 44 | } 45 | })(err => { 46 | removeFailedScript() 47 | onComplete(err) 48 | }) 49 | } 50 | 51 | const addCache = (entry) => { 52 | if (loadedScript.indexOf(entry) < 0) { 53 | loadedScript.push(entry) 54 | } 55 | } 56 | 57 | const removeFailedScript = () => { 58 | if (failedScript.length > 0) { 59 | failedScript.forEach((script) => { 60 | const node = document.querySelector(`script[src='${script}']`) 61 | if (node != null) { 62 | node.parentNode.removeChild(node) 63 | } 64 | }) 65 | 66 | failedScript = [] 67 | } 68 | } 69 | 70 | const scriptLoader = (...scripts) => (WrappedComponent) => { 71 | class ScriptLoader extends Component { 72 | static propTypes = { 73 | onScriptLoaded: PropTypes.func 74 | } 75 | 76 | static defaultProps = { 77 | onScriptLoaded: noop 78 | } 79 | 80 | constructor (props, context) { 81 | super(props, context) 82 | 83 | this.state = { 84 | isScriptLoaded: false, 85 | isScriptLoadSucceed: false 86 | } 87 | 88 | this._isMounted = false; 89 | } 90 | 91 | componentDidMount () { 92 | this._isMounted = true; 93 | startLoadingScripts(scripts, err => { 94 | if(this._isMounted) { 95 | this.setState({ 96 | isScriptLoaded: true, 97 | isScriptLoadSucceed: !err 98 | }, () => { 99 | if (!err) { 100 | this.props.onScriptLoaded() 101 | } 102 | }) 103 | } 104 | }) 105 | } 106 | 107 | componentWillUnmount () { 108 | this._isMounted = false; 109 | } 110 | 111 | getWrappedInstance () { 112 | return this.refs.wrappedInstance; 113 | } 114 | 115 | render () { 116 | const props = { 117 | ...this.props, 118 | ...this.state, 119 | ref: 'wrappedInstance' 120 | } 121 | 122 | return ( 123 | 124 | ) 125 | } 126 | } 127 | 128 | return hoistStatics(ScriptLoader, WrappedComponent) 129 | } 130 | 131 | export default scriptLoader 132 | --------------------------------------------------------------------------------