Skip to content

Commit 330a2c4

Browse files
authored
Merge pull request #2762 from plotly/feat/dynamic-loading
Dynamic loading of component libraries.
2 parents ba22c4d + 8e4b4fe commit 330a2c4

24 files changed

+666
-113
lines changed

Diff for: @plotly/dash-test-components/src/components/StyledComponent.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ StyledComponent.propTypes = {
1515
/**
1616
* The style
1717
*/
18-
style: PropTypes.shape,
18+
style: PropTypes.object,
1919

2020
/**
2121
* The value to display
@@ -27,4 +27,4 @@ StyledComponent.defaultProps = {
2727
value: ''
2828
};
2929

30-
export default StyledComponent;
30+
export default StyledComponent;

Diff for: CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ This project adheres to [Semantic Versioning](https://semver.org/).
1515
- [#2735](https://github.com/plotly/dash/pull/2735) Configure CI for Python 3.8 and 3.12, drop support for Python 3.6 and Python 3.7 [#2736](https://github.com/plotly/dash/issues/2736)
1616

1717
## Added
18+
- [#2762](https://github.com/plotly/dash/pull/2762) Add dynamic loading of component libraries.
19+
- Add `dynamic_loading=True` to dash init.
20+
- Add `preloaded_libraries=[]` to dash init, included libraries names will be loaded on the index like before.
1821
- [#2758](https://github.com/plotly/dash/pull/2758)
1922
- exposing `setProps` to `dash_clientside.clientSide_setProps` to allow for JS code to interact directly with the dash eco-system
2023
- [#2730](https://github.com/plotly/dash/pull/2721) Load script files with `.mjs` ending as js modules

Diff for: dash/dash-renderer/src/APIController.react.js

+34-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import {batch, connect} from 'react-redux';
22
import {includes, isEmpty} from 'ramda';
3-
import React, {useEffect, useRef, useState, createContext} from 'react';
3+
import React, {
4+
useEffect,
5+
useRef,
6+
useState,
7+
createContext,
8+
useCallback
9+
} from 'react';
410
import PropTypes from 'prop-types';
511
import TreeContainer from './TreeContainer';
612
import GlobalErrorContainer from './components/error/GlobalErrorContainer.react';
@@ -21,6 +27,7 @@ import {getAppState} from './reducers/constants';
2127
import {STATUS} from './constants/constants';
2228
import {getLoadingState, getLoadingHash} from './utils/TreeContainer';
2329
import wait from './utils/wait';
30+
import LibraryManager from './libraries/LibraryManager';
2431

2532
export const DashContext = createContext({});
2633

@@ -46,6 +53,10 @@ const UnconnectedContainer = props => {
4653
if (!events.current) {
4754
events.current = new EventEmitter();
4855
}
56+
57+
const [libraryReady, setLibraryReady] = useState(false);
58+
const onLibraryReady = useCallback(() => setLibraryReady(true), []);
59+
4960
const renderedTree = useRef(false);
5061

5162
const propsRef = useRef({});
@@ -60,7 +71,9 @@ const UnconnectedContainer = props => {
6071
})
6172
});
6273

63-
useEffect(storeEffect.bind(null, props, events, setErrorLoading));
74+
useEffect(
75+
storeEffect.bind(null, props, events, setErrorLoading, libraryReady)
76+
);
6477

6578
useEffect(() => {
6679
if (renderedTree.current) {
@@ -117,14 +130,23 @@ const UnconnectedContainer = props => {
117130
content = <div className='_dash-loading'>Loading...</div>;
118131
}
119132

120-
return config && config.ui === true ? (
121-
<GlobalErrorContainer>{content}</GlobalErrorContainer>
122-
) : (
123-
content
133+
return (
134+
<LibraryManager
135+
requests_pathname_prefix={config.requests_pathname_prefix}
136+
onReady={onLibraryReady}
137+
ready={libraryReady}
138+
layout={layoutRequest && layoutRequest.content}
139+
>
140+
{config && config.ui === true ? (
141+
<GlobalErrorContainer>{content}</GlobalErrorContainer>
142+
) : (
143+
content
144+
)}
145+
</LibraryManager>
124146
);
125147
};
126148

127-
function storeEffect(props, events, setErrorLoading) {
149+
function storeEffect(props, events, setErrorLoading, libraryReady) {
128150
const {
129151
appLifecycle,
130152
dependenciesRequest,
@@ -143,7 +165,7 @@ function storeEffect(props, events, setErrorLoading) {
143165
}
144166
dispatch(apiThunk('_dash-layout', 'GET', 'layoutRequest'));
145167
} else if (layoutRequest.status === STATUS.OK) {
146-
if (isEmpty(layout)) {
168+
if (isEmpty(layout) && libraryReady) {
147169
if (typeof hooks.layout_post === 'function') {
148170
hooks.layout_post(layoutRequest.content);
149171
}
@@ -186,7 +208,8 @@ function storeEffect(props, events, setErrorLoading) {
186208
layoutRequest.status === STATUS.OK &&
187209
!isEmpty(layout) &&
188210
// Hasn't already hydrated
189-
appLifecycle === getAppState('STARTED')
211+
appLifecycle === getAppState('STARTED') &&
212+
libraryReady
190213
) {
191214
let hasError = false;
192215
try {
@@ -235,7 +258,8 @@ const Container = connect(
235258
graphs: state.graphs,
236259
history: state.history,
237260
error: state.error,
238-
config: state.config
261+
config: state.config,
262+
paths: state.paths
239263
}),
240264
dispatch => ({dispatch})
241265
)(UnconnectedContainer);

Diff for: dash/dash-renderer/src/CheckedComponent.react.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import checkPropTypes from './checkPropTypes';
2+
import {propTypeErrorHandler} from './exceptions';
3+
import {createLibraryElement} from './libraries/createLibraryElement';
4+
import PropTypes from 'prop-types';
5+
6+
export function CheckedComponent(p) {
7+
const {element, extraProps, props, children, type} = p;
8+
9+
const errorMessage = checkPropTypes(
10+
element.propTypes,
11+
props,
12+
'component prop',
13+
element
14+
);
15+
if (errorMessage) {
16+
propTypeErrorHandler(errorMessage, props, type);
17+
}
18+
19+
return createLibraryElement(element, props, extraProps, children);
20+
}
21+
22+
CheckedComponent.propTypes = {
23+
children: PropTypes.any,
24+
element: PropTypes.any,
25+
layout: PropTypes.any,
26+
props: PropTypes.any,
27+
extraProps: PropTypes.any,
28+
id: PropTypes.string,
29+
type: PropTypes.string
30+
};

Diff for: dash/dash-renderer/src/TreeContainer.js

+14-54
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,44 @@
11
import React, {Component, memo, useContext} from 'react';
22
import PropTypes from 'prop-types';
3-
import Registry from './registry';
4-
import {propTypeErrorHandler} from './exceptions';
53
import {
64
addIndex,
75
assoc,
86
assocPath,
97
concat,
108
dissoc,
119
equals,
10+
has,
1211
isEmpty,
1312
isNil,
14-
has,
1513
keys,
1614
map,
1715
mapObjIndexed,
18-
mergeRight,
16+
path as rpath,
17+
pathOr,
1918
pick,
2019
pickBy,
2120
propOr,
22-
path as rpath,
23-
pathOr,
2421
type
2522
} from 'ramda';
23+
import {batch} from 'react-redux';
24+
2625
import {notifyObservers, updateProps, onError} from './actions';
2726
import isSimpleComponent from './isSimpleComponent';
2827
import {recordUiEdit} from './persistence';
2928
import ComponentErrorBoundary from './components/error/ComponentErrorBoundary.react';
30-
import checkPropTypes from './checkPropTypes';
3129
import {getWatchedKeys, stringifyId} from './actions/dependencies';
3230
import {
3331
getLoadingHash,
3432
getLoadingState,
3533
validateComponent
3634
} from './utils/TreeContainer';
3735
import {DashContext} from './APIController.react';
38-
import {batch} from 'react-redux';
36+
import LibraryComponent from './libraries/LibraryComponent';
3937

4038
const NOT_LOADING = {
4139
is_loading: false
4240
};
4341

44-
function CheckedComponent(p) {
45-
const {element, extraProps, props, children, type} = p;
46-
47-
const errorMessage = checkPropTypes(
48-
element.propTypes,
49-
props,
50-
'component prop',
51-
element
52-
);
53-
if (errorMessage) {
54-
propTypeErrorHandler(errorMessage, props, type);
55-
}
56-
57-
return createElement(element, props, extraProps, children);
58-
}
59-
60-
CheckedComponent.propTypes = {
61-
children: PropTypes.any,
62-
element: PropTypes.any,
63-
layout: PropTypes.any,
64-
props: PropTypes.any,
65-
extraProps: PropTypes.any,
66-
id: PropTypes.string
67-
};
68-
69-
function createElement(element, props, extraProps, children) {
70-
const allProps = mergeRight(props, extraProps);
71-
if (Array.isArray(children)) {
72-
return React.createElement(element, allProps, ...children);
73-
}
74-
return React.createElement(element, allProps, children);
75-
}
76-
7742
function isDryComponent(obj) {
7843
return (
7944
type(obj) === 'Object' &&
@@ -250,8 +215,6 @@ class BaseTreeContainer extends Component {
250215
}
251216
validateComponent(_dashprivate_layout);
252217

253-
const element = Registry.resolve(_dashprivate_layout);
254-
255218
// Hydrate components props
256219
const childrenProps = pathOr(
257220
[],
@@ -455,17 +418,14 @@ class BaseTreeContainer extends Component {
455418
dispatch={_dashprivate_dispatch}
456419
error={_dashprivate_error}
457420
>
458-
{_dashprivate_config.props_check ? (
459-
<CheckedComponent
460-
children={children}
461-
element={element}
462-
props={props}
463-
extraProps={extraProps}
464-
type={_dashprivate_layout.type}
465-
/>
466-
) : (
467-
createElement(element, props, extraProps, children)
468-
)}
421+
<LibraryComponent
422+
children={children}
423+
type={_dashprivate_layout.type}
424+
namespace={_dashprivate_layout.namespace}
425+
props={props}
426+
extraProps={extraProps}
427+
props_check={_dashprivate_config.props_check}
428+
/>
469429
</ComponentErrorBoundary>
470430
);
471431
}

Diff for: dash/dash-renderer/src/actions/callbacks.ts

+40-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import {
3434
CallbackResponseData
3535
} from '../types/callbacks';
3636
import {isMultiValued, stringifyId, isMultiOutputProp} from './dependencies';
37-
import {urlBase} from './utils';
37+
import {crawlLayout, urlBase} from './utils';
3838
import {getCSRFHeader} from '.';
3939
import {createAction, Action} from 'redux-actions';
4040
import {addHttpHeaders} from '../actions';
@@ -44,6 +44,9 @@ import {handlePatch, isPatch} from './patch';
4444
import {getPath} from './paths';
4545

4646
import {requestDependencies} from './requestDependencies';
47+
import loadLibrary from '../libraries/loadLibrary';
48+
import fetchDist from '../libraries/fetchDist';
49+
import {setLibraryLoaded} from './libraries';
4750

4851
export const addBlockedCallbacks = createAction<IBlockedCallback[]>(
4952
CallbackActionType.AddBlocked
@@ -363,6 +366,7 @@ function handleServerside(
363366
let runningOff: any;
364367
let progressDefault: any;
365368
let moreArgs = additionalArgs;
369+
const libraries = Object.keys(getState().libraries);
366370

367371
if (running) {
368372
sideUpdate(running.running, dispatch, paths);
@@ -508,8 +512,41 @@ function handleServerside(
508512
}
509513

510514
if (!long || data.response !== undefined) {
511-
completeJob();
512-
finishLine(data);
515+
const newLibs: string[] = [];
516+
Object.values(data.response as any).forEach(
517+
(newData: any) => {
518+
Object.values(newData).forEach(newProp => {
519+
crawlLayout(newProp, (c: any) => {
520+
if (
521+
c.namespace &&
522+
!libraries.includes(c.namespace) &&
523+
!newLibs.includes(c.namespace)
524+
) {
525+
newLibs.push(c.namespace);
526+
}
527+
});
528+
});
529+
}
530+
);
531+
if (newLibs.length) {
532+
fetchDist(
533+
getState().config.requests_pathname_prefix,
534+
newLibs
535+
)
536+
.then(data => {
537+
return Promise.all(data.map(loadLibrary));
538+
})
539+
.then(() => {
540+
completeJob();
541+
finishLine(data);
542+
dispatch(
543+
setLibraryLoaded({libraries: newLibs})
544+
);
545+
});
546+
} else {
547+
completeJob();
548+
finishLine(data);
549+
}
513550
} else {
514551
// Poll chain.
515552
setTimeout(

Diff for: dash/dash-renderer/src/actions/libraries.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {LibrariesActions} from '../libraries/libraryTypes';
2+
3+
const createAction = (type: LibrariesActions) => (payload: any) => ({
4+
type,
5+
payload
6+
});
7+
8+
export const setLibraryLoading = createAction(LibrariesActions.LOAD);
9+
export const setLibraryLoaded = createAction(LibrariesActions.LOADED);
10+
export const setLibraryToLoad = createAction(LibrariesActions.TO_LOAD);

0 commit comments

Comments
 (0)