diff --git a/@plotly/dash-test-components/src/components/StyledComponent.js b/@plotly/dash-test-components/src/components/StyledComponent.js
index f7e8703f40..f48dbf7884 100644
--- a/@plotly/dash-test-components/src/components/StyledComponent.js
+++ b/@plotly/dash-test-components/src/components/StyledComponent.js
@@ -15,7 +15,7 @@ StyledComponent.propTypes = {
/**
* The style
*/
- style: PropTypes.shape,
+ style: PropTypes.object,
/**
* The value to display
@@ -27,4 +27,4 @@ StyledComponent.defaultProps = {
value: ''
};
-export default StyledComponent;
\ No newline at end of file
+export default StyledComponent;
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c993fdd65..1a8431843a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,9 @@ This project adheres to [Semantic Versioning](https://semver.org/).
- [#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)
## Added
+- [#2762](https://github.com/plotly/dash/pull/2762) Add dynamic loading of component libraries.
+ - Add `dynamic_loading=True` to dash init.
+ - Add `preloaded_libraries=[]` to dash init, included libraries names will be loaded on the index like before.
- [#2758](https://github.com/plotly/dash/pull/2758)
- exposing `setProps` to `dash_clientside.clientSide_setProps` to allow for JS code to interact directly with the dash eco-system
- [#2730](https://github.com/plotly/dash/pull/2721) Load script files with `.mjs` ending as js modules
diff --git a/dash/dash-renderer/src/APIController.react.js b/dash/dash-renderer/src/APIController.react.js
index ed443bad5c..9e90dc6268 100644
--- a/dash/dash-renderer/src/APIController.react.js
+++ b/dash/dash-renderer/src/APIController.react.js
@@ -1,6 +1,12 @@
import {batch, connect} from 'react-redux';
import {includes, isEmpty} from 'ramda';
-import React, {useEffect, useRef, useState, createContext} from 'react';
+import React, {
+ useEffect,
+ useRef,
+ useState,
+ createContext,
+ useCallback
+} from 'react';
import PropTypes from 'prop-types';
import TreeContainer from './TreeContainer';
import GlobalErrorContainer from './components/error/GlobalErrorContainer.react';
@@ -21,6 +27,7 @@ import {getAppState} from './reducers/constants';
import {STATUS} from './constants/constants';
import {getLoadingState, getLoadingHash} from './utils/TreeContainer';
import wait from './utils/wait';
+import LibraryManager from './libraries/LibraryManager';
export const DashContext = createContext({});
@@ -46,6 +53,10 @@ const UnconnectedContainer = props => {
if (!events.current) {
events.current = new EventEmitter();
}
+
+ const [libraryReady, setLibraryReady] = useState(false);
+ const onLibraryReady = useCallback(() => setLibraryReady(true), []);
+
const renderedTree = useRef(false);
const propsRef = useRef({});
@@ -60,7 +71,9 @@ const UnconnectedContainer = props => {
})
});
- useEffect(storeEffect.bind(null, props, events, setErrorLoading));
+ useEffect(
+ storeEffect.bind(null, props, events, setErrorLoading, libraryReady)
+ );
useEffect(() => {
if (renderedTree.current) {
@@ -117,14 +130,23 @@ const UnconnectedContainer = props => {
content =
Loading...
;
}
- return config && config.ui === true ? (
- {content}
- ) : (
- content
+ return (
+
+ {config && config.ui === true ? (
+ {content}
+ ) : (
+ content
+ )}
+
);
};
-function storeEffect(props, events, setErrorLoading) {
+function storeEffect(props, events, setErrorLoading, libraryReady) {
const {
appLifecycle,
dependenciesRequest,
@@ -143,7 +165,7 @@ function storeEffect(props, events, setErrorLoading) {
}
dispatch(apiThunk('_dash-layout', 'GET', 'layoutRequest'));
} else if (layoutRequest.status === STATUS.OK) {
- if (isEmpty(layout)) {
+ if (isEmpty(layout) && libraryReady) {
if (typeof hooks.layout_post === 'function') {
hooks.layout_post(layoutRequest.content);
}
@@ -186,7 +208,8 @@ function storeEffect(props, events, setErrorLoading) {
layoutRequest.status === STATUS.OK &&
!isEmpty(layout) &&
// Hasn't already hydrated
- appLifecycle === getAppState('STARTED')
+ appLifecycle === getAppState('STARTED') &&
+ libraryReady
) {
let hasError = false;
try {
@@ -235,7 +258,8 @@ const Container = connect(
graphs: state.graphs,
history: state.history,
error: state.error,
- config: state.config
+ config: state.config,
+ paths: state.paths
}),
dispatch => ({dispatch})
)(UnconnectedContainer);
diff --git a/dash/dash-renderer/src/CheckedComponent.react.js b/dash/dash-renderer/src/CheckedComponent.react.js
new file mode 100644
index 0000000000..4eeed6d524
--- /dev/null
+++ b/dash/dash-renderer/src/CheckedComponent.react.js
@@ -0,0 +1,30 @@
+import checkPropTypes from './checkPropTypes';
+import {propTypeErrorHandler} from './exceptions';
+import {createLibraryElement} from './libraries/createLibraryElement';
+import PropTypes from 'prop-types';
+
+export function CheckedComponent(p) {
+ const {element, extraProps, props, children, type} = p;
+
+ const errorMessage = checkPropTypes(
+ element.propTypes,
+ props,
+ 'component prop',
+ element
+ );
+ if (errorMessage) {
+ propTypeErrorHandler(errorMessage, props, type);
+ }
+
+ return createLibraryElement(element, props, extraProps, children);
+}
+
+CheckedComponent.propTypes = {
+ children: PropTypes.any,
+ element: PropTypes.any,
+ layout: PropTypes.any,
+ props: PropTypes.any,
+ extraProps: PropTypes.any,
+ id: PropTypes.string,
+ type: PropTypes.string
+};
diff --git a/dash/dash-renderer/src/TreeContainer.js b/dash/dash-renderer/src/TreeContainer.js
index 304cd0be74..b79980e9a8 100644
--- a/dash/dash-renderer/src/TreeContainer.js
+++ b/dash/dash-renderer/src/TreeContainer.js
@@ -1,7 +1,5 @@
import React, {Component, memo, useContext} from 'react';
import PropTypes from 'prop-types';
-import Registry from './registry';
-import {propTypeErrorHandler} from './exceptions';
import {
addIndex,
assoc,
@@ -9,25 +7,25 @@ import {
concat,
dissoc,
equals,
+ has,
isEmpty,
isNil,
- has,
keys,
map,
mapObjIndexed,
- mergeRight,
+ path as rpath,
+ pathOr,
pick,
pickBy,
propOr,
- path as rpath,
- pathOr,
type
} from 'ramda';
+import {batch} from 'react-redux';
+
import {notifyObservers, updateProps, onError} from './actions';
import isSimpleComponent from './isSimpleComponent';
import {recordUiEdit} from './persistence';
import ComponentErrorBoundary from './components/error/ComponentErrorBoundary.react';
-import checkPropTypes from './checkPropTypes';
import {getWatchedKeys, stringifyId} from './actions/dependencies';
import {
getLoadingHash,
@@ -35,45 +33,12 @@ import {
validateComponent
} from './utils/TreeContainer';
import {DashContext} from './APIController.react';
-import {batch} from 'react-redux';
+import LibraryComponent from './libraries/LibraryComponent';
const NOT_LOADING = {
is_loading: false
};
-function CheckedComponent(p) {
- const {element, extraProps, props, children, type} = p;
-
- const errorMessage = checkPropTypes(
- element.propTypes,
- props,
- 'component prop',
- element
- );
- if (errorMessage) {
- propTypeErrorHandler(errorMessage, props, type);
- }
-
- return createElement(element, props, extraProps, children);
-}
-
-CheckedComponent.propTypes = {
- children: PropTypes.any,
- element: PropTypes.any,
- layout: PropTypes.any,
- props: PropTypes.any,
- extraProps: PropTypes.any,
- id: PropTypes.string
-};
-
-function createElement(element, props, extraProps, children) {
- const allProps = mergeRight(props, extraProps);
- if (Array.isArray(children)) {
- return React.createElement(element, allProps, ...children);
- }
- return React.createElement(element, allProps, children);
-}
-
function isDryComponent(obj) {
return (
type(obj) === 'Object' &&
@@ -250,8 +215,6 @@ class BaseTreeContainer extends Component {
}
validateComponent(_dashprivate_layout);
- const element = Registry.resolve(_dashprivate_layout);
-
// Hydrate components props
const childrenProps = pathOr(
[],
@@ -455,17 +418,14 @@ class BaseTreeContainer extends Component {
dispatch={_dashprivate_dispatch}
error={_dashprivate_error}
>
- {_dashprivate_config.props_check ? (
-
- ) : (
- createElement(element, props, extraProps, children)
- )}
+
);
}
diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts
index 68320568e5..e1e4b4601b 100644
--- a/dash/dash-renderer/src/actions/callbacks.ts
+++ b/dash/dash-renderer/src/actions/callbacks.ts
@@ -34,7 +34,7 @@ import {
CallbackResponseData
} from '../types/callbacks';
import {isMultiValued, stringifyId, isMultiOutputProp} from './dependencies';
-import {urlBase} from './utils';
+import {crawlLayout, urlBase} from './utils';
import {getCSRFHeader} from '.';
import {createAction, Action} from 'redux-actions';
import {addHttpHeaders} from '../actions';
@@ -44,6 +44,9 @@ import {handlePatch, isPatch} from './patch';
import {getPath} from './paths';
import {requestDependencies} from './requestDependencies';
+import loadLibrary from '../libraries/loadLibrary';
+import fetchDist from '../libraries/fetchDist';
+import {setLibraryLoaded} from './libraries';
export const addBlockedCallbacks = createAction(
CallbackActionType.AddBlocked
@@ -363,6 +366,7 @@ function handleServerside(
let runningOff: any;
let progressDefault: any;
let moreArgs = additionalArgs;
+ const libraries = Object.keys(getState().libraries);
if (running) {
sideUpdate(running.running, dispatch, paths);
@@ -508,8 +512,41 @@ function handleServerside(
}
if (!long || data.response !== undefined) {
- completeJob();
- finishLine(data);
+ const newLibs: string[] = [];
+ Object.values(data.response as any).forEach(
+ (newData: any) => {
+ Object.values(newData).forEach(newProp => {
+ crawlLayout(newProp, (c: any) => {
+ if (
+ c.namespace &&
+ !libraries.includes(c.namespace) &&
+ !newLibs.includes(c.namespace)
+ ) {
+ newLibs.push(c.namespace);
+ }
+ });
+ });
+ }
+ );
+ if (newLibs.length) {
+ fetchDist(
+ getState().config.requests_pathname_prefix,
+ newLibs
+ )
+ .then(data => {
+ return Promise.all(data.map(loadLibrary));
+ })
+ .then(() => {
+ completeJob();
+ finishLine(data);
+ dispatch(
+ setLibraryLoaded({libraries: newLibs})
+ );
+ });
+ } else {
+ completeJob();
+ finishLine(data);
+ }
} else {
// Poll chain.
setTimeout(
diff --git a/dash/dash-renderer/src/actions/libraries.ts b/dash/dash-renderer/src/actions/libraries.ts
new file mode 100644
index 0000000000..5cac6b3597
--- /dev/null
+++ b/dash/dash-renderer/src/actions/libraries.ts
@@ -0,0 +1,10 @@
+import {LibrariesActions} from '../libraries/libraryTypes';
+
+const createAction = (type: LibrariesActions) => (payload: any) => ({
+ type,
+ payload
+});
+
+export const setLibraryLoading = createAction(LibrariesActions.LOAD);
+export const setLibraryLoaded = createAction(LibrariesActions.LOADED);
+export const setLibraryToLoad = createAction(LibrariesActions.TO_LOAD);
diff --git a/dash/dash-renderer/src/libraries/LibraryComponent.tsx b/dash/dash-renderer/src/libraries/LibraryComponent.tsx
new file mode 100644
index 0000000000..0b8b18104c
--- /dev/null
+++ b/dash/dash-renderer/src/libraries/LibraryComponent.tsx
@@ -0,0 +1,47 @@
+import React, {useContext, useEffect} from 'react';
+import {LibrariesContext} from './librariesContext';
+import Registry from '../registry';
+import {CheckedComponent} from '../CheckedComponent.react';
+import {createLibraryElement} from './createLibraryElement';
+
+type LibraryComponentProps = {
+ type: string;
+ namespace: string;
+ props: any;
+ extraProps: any;
+ children: any;
+ props_check: boolean;
+};
+
+const LibraryComponent = (props: LibraryComponentProps) => {
+ const {props_check, namespace, type, ...rest} = props;
+
+ const context = useContext(LibrariesContext);
+
+ useEffect(() => {
+ context.addToLoad(namespace);
+ }, []);
+
+ if (!context.isLoaded(namespace)) {
+ return <>>;
+ }
+ const element = Registry.resolve({namespace, type});
+ if (props_check) {
+ return (
+
+ );
+ }
+ return createLibraryElement(
+ element,
+ rest.props,
+ rest.extraProps,
+ rest.children
+ );
+};
+export default LibraryComponent;
diff --git a/dash/dash-renderer/src/libraries/LibraryManager.tsx b/dash/dash-renderer/src/libraries/LibraryManager.tsx
new file mode 100644
index 0000000000..ad248be751
--- /dev/null
+++ b/dash/dash-renderer/src/libraries/LibraryManager.tsx
@@ -0,0 +1,67 @@
+import React, {JSX, useEffect, useState} from 'react';
+
+import {createLibrariesContext, LibrariesContext} from './librariesContext';
+import {crawlLayout} from '../actions/utils';
+import {isEmpty} from 'ramda';
+
+type LibrariesManagerProps = {
+ children: JSX.Element;
+ requests_pathname_prefix: string;
+ onReady: () => void;
+ ready: boolean;
+ layout?: any;
+ initialLibraries?: string[];
+};
+
+const LibraryProvider = (props: LibrariesManagerProps) => {
+ const {
+ children,
+ requests_pathname_prefix,
+ onReady,
+ ready,
+ initialLibraries
+ } = props;
+ const contextValue = createLibrariesContext(
+ requests_pathname_prefix,
+ initialLibraries as string[],
+ onReady,
+ ready
+ );
+ return (
+
+ {children}
+
+ );
+};
+
+const LibraryManager = (props: LibrariesManagerProps) => {
+ const {children, ready, layout} = props;
+
+ const [initialLibraries, setInitialLibraries] = useState(
+ null
+ );
+
+ useEffect(() => {
+ if (layout && !isEmpty(layout) && !ready && !initialLibraries) {
+ const libraries: string[] = [];
+ crawlLayout(layout, (child: any) => {
+ if (child.namespace && !libraries.includes(child.namespace)) {
+ libraries.push(child.namespace);
+ }
+ });
+ setInitialLibraries(libraries);
+ }
+ }, [layout, ready, initialLibraries]);
+
+ if (!initialLibraries) {
+ return children;
+ }
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default LibraryManager;
diff --git a/dash/dash-renderer/src/libraries/createLibraryElement.js b/dash/dash-renderer/src/libraries/createLibraryElement.js
new file mode 100644
index 0000000000..e5ebe6fa68
--- /dev/null
+++ b/dash/dash-renderer/src/libraries/createLibraryElement.js
@@ -0,0 +1,10 @@
+import {mergeRight} from 'ramda';
+import React from 'react';
+
+export function createLibraryElement(element, props, extraProps, children) {
+ const allProps = mergeRight(props, extraProps);
+ if (Array.isArray(children)) {
+ return React.createElement(element, allProps, ...children);
+ }
+ return React.createElement(element, allProps, children);
+}
diff --git a/dash/dash-renderer/src/libraries/fetchDist.ts b/dash/dash-renderer/src/libraries/fetchDist.ts
new file mode 100644
index 0000000000..d82841e6d2
--- /dev/null
+++ b/dash/dash-renderer/src/libraries/fetchDist.ts
@@ -0,0 +1,12 @@
+import {LibraryResource} from './libraryTypes';
+
+export default function fetchDist(
+ pathnamePrefix: string,
+ libraries: string[]
+): Promise {
+ return fetch(`${pathnamePrefix}_dash-dist`, {
+ body: JSON.stringify(libraries),
+ headers: {'Content-Type': 'application/json'},
+ method: 'POST'
+ }).then(response => response.json());
+}
diff --git a/dash/dash-renderer/src/libraries/librariesContext.ts b/dash/dash-renderer/src/libraries/librariesContext.ts
new file mode 100644
index 0000000000..ed871850c8
--- /dev/null
+++ b/dash/dash-renderer/src/libraries/librariesContext.ts
@@ -0,0 +1,132 @@
+import {createContext, useEffect, useState} from 'react';
+import {pathOr, toPairs} from 'ramda';
+import loadLibrary from './loadLibrary';
+import {batch, useDispatch, useSelector} from 'react-redux';
+import {LibrariesState} from './libraryTypes';
+import {
+ setLibraryLoaded,
+ setLibraryLoading,
+ setLibraryToLoad
+} from '../actions/libraries';
+import fetchDist from './fetchDist';
+
+export type LibrariesContextType = {
+ state: LibrariesState;
+ isLoading: (libraryName: string) => boolean;
+ isLoaded: (libraryName: string) => boolean;
+ fetchLibraries: () => void;
+ getLibrariesToLoad: () => string[];
+ addToLoad: (libName: string) => void;
+};
+
+function librarySelector(s: any) {
+ return s.libraries as LibrariesState;
+}
+
+export function createLibrariesContext(
+ pathnamePrefix: string,
+ initialLibraries: string[],
+ onReady: () => void,
+ ready: boolean
+): LibrariesContextType {
+ const dispatch = useDispatch();
+ const state = useSelector(librarySelector);
+ const [callback, setCallback] = useState(-1);
+
+ const isLoaded = (libraryName: string) =>
+ pathOr(false, [libraryName, 'loaded'], state);
+ const isLoading = (libraryName: string) =>
+ pathOr(false, [libraryName, 'loading'], state);
+
+ const addToLoad = (libraryName: string) => {
+ const lib = state[libraryName];
+ if (!lib) {
+ // Check if already loaded on the window
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ if (window[libraryName]) {
+ dispatch(setLibraryLoaded({libraries: [libraryName]}));
+ } else {
+ dispatch(setLibraryToLoad({library: libraryName}));
+ }
+ }
+ // if lib is already in don't do anything.
+ };
+
+ const getLibrariesToLoad = () =>
+ toPairs(state).reduce((acc: string[], [key, value]) => {
+ if (value.toLoad) {
+ acc.push(key);
+ }
+ return acc;
+ }, []);
+
+ const fetchLibraries = () => {
+ const libraries = getLibrariesToLoad();
+ if (!libraries.length) {
+ return;
+ }
+
+ dispatch(setLibraryLoading({libraries}));
+
+ fetchDist(pathnamePrefix, libraries)
+ .then(data => {
+ return Promise.all(data.map(loadLibrary));
+ })
+ .then(() => {
+ dispatch(setLibraryLoaded({libraries}));
+ setCallback(-1);
+ onReady();
+ });
+ };
+
+ useEffect(() => {
+ batch(() => {
+ const loaded: string[] = [];
+ initialLibraries.forEach(lib => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ if (window[lib]) {
+ loaded.push(lib);
+ } else {
+ dispatch(setLibraryToLoad({library: lib}));
+ }
+ });
+ if (loaded.length) {
+ dispatch(setLibraryLoaded({libraries: loaded}));
+ }
+ if (loaded.length === initialLibraries.length) {
+ onReady();
+ }
+ });
+ }, [initialLibraries]);
+
+ // Load libraries on a throttle to have time to gather all the components in one go.
+ useEffect(() => {
+ if (ready) {
+ return;
+ }
+ const libraries = getLibrariesToLoad();
+ if (!libraries.length) {
+ return;
+ }
+ if (callback > 0) {
+ window.clearTimeout(callback);
+ }
+ const timeout = window.setTimeout(fetchLibraries, 0);
+ setCallback(timeout);
+ }, [state, ready, initialLibraries]);
+
+ return {
+ state,
+ isLoaded,
+ isLoading,
+ fetchLibraries,
+ getLibrariesToLoad,
+ addToLoad
+ };
+}
+
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+export const LibrariesContext = createContext(null);
diff --git a/dash/dash-renderer/src/libraries/libraryTypes.ts b/dash/dash-renderer/src/libraries/libraryTypes.ts
new file mode 100644
index 0000000000..b1c5f4887c
--- /dev/null
+++ b/dash/dash-renderer/src/libraries/libraryTypes.ts
@@ -0,0 +1,17 @@
+export enum LibrariesActions {
+ LOAD = 'LOAD_LIBRARY',
+ LOADED = 'LOADED_LIBRARY',
+ TO_LOAD = 'TO_LOAD'
+}
+
+export type LibrariesState = {
+ [libname: string]: {
+ toLoad: boolean;
+ loading: boolean;
+ loaded: boolean;
+ };
+};
+export type LibraryResource = {
+ type: '_js_dist' | '_css_dist';
+ url: string;
+};
diff --git a/dash/dash-renderer/src/libraries/loadLibrary.ts b/dash/dash-renderer/src/libraries/loadLibrary.ts
new file mode 100644
index 0000000000..0f83670c1d
--- /dev/null
+++ b/dash/dash-renderer/src/libraries/loadLibrary.ts
@@ -0,0 +1,31 @@
+import {LibraryResource} from './libraryTypes';
+
+export default function (resource: LibraryResource) {
+ let prom;
+ const head = document.querySelector('head');
+ if (resource.type === '_js_dist') {
+ const element = document.createElement('script');
+ element.src = resource.url;
+ element.async = true;
+ prom = new Promise((resolve, reject) => {
+ element.onload = () => {
+ resolve();
+ };
+ element.onerror = error => reject(error);
+ });
+
+ head?.appendChild(element);
+ } else if (resource.type === '_css_dist') {
+ const element = document.createElement('link');
+ element.href = resource.url;
+ element.rel = 'stylesheet';
+ prom = new Promise((resolve, reject) => {
+ element.onload = () => {
+ resolve();
+ };
+ element.onerror = error => reject(error);
+ });
+ head?.appendChild(element);
+ }
+ return prom;
+}
diff --git a/dash/dash-renderer/src/reducers/libraries.ts b/dash/dash-renderer/src/reducers/libraries.ts
new file mode 100644
index 0000000000..421108b486
--- /dev/null
+++ b/dash/dash-renderer/src/reducers/libraries.ts
@@ -0,0 +1,60 @@
+import {assocPath, pipe} from 'ramda';
+import {LibrariesActions, LibrariesState} from '../libraries/libraryTypes';
+
+type LoadingPayload = {
+ libraries: string[];
+};
+
+type LoadedPayload = {
+ libraries: string[];
+};
+
+type ToLoadPayload = {
+ library: string;
+};
+
+type LibrariesAction = {
+ type: LibrariesActions;
+ payload: LoadingPayload | LoadedPayload | ToLoadPayload;
+};
+
+function handleLoad(library: string, state: LibrariesState) {
+ return pipe(
+ assocPath([library, 'loading'], true),
+ assocPath([library, 'toLoad'], false)
+ )(state) as LibrariesState;
+}
+
+function handleLoaded(library: string, state: LibrariesState) {
+ return pipe(
+ assocPath([library, 'loaded'], true),
+ assocPath([library, 'loading'], false)
+ )(state) as LibrariesState;
+}
+
+export default function librariesReducer(
+ state: LibrariesState = {},
+ action: LibrariesAction
+): LibrariesState {
+ switch (action.type) {
+ case LibrariesActions.LOAD:
+ return (action.payload as LoadingPayload).libraries.reduce(
+ (acc, lib) => handleLoad(lib, acc),
+ state
+ );
+ case LibrariesActions.LOADED:
+ return (action.payload as LoadedPayload).libraries.reduce(
+ (acc, lib) => handleLoaded(lib, acc),
+ state
+ );
+ case LibrariesActions.TO_LOAD:
+ return pipe(
+ assocPath(
+ [(action.payload as ToLoadPayload).library, 'toLoad'],
+ true
+ )
+ )(state) as LibrariesState;
+ default:
+ return state;
+ }
+}
diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js
index 97b71b6bce..f025d27354 100644
--- a/dash/dash-renderer/src/reducers/reducer.js
+++ b/dash/dash-renderer/src/reducers/reducer.js
@@ -18,6 +18,7 @@ import layout from './layout';
import loadingMap from './loadingMap';
import paths from './paths';
import callbackJobs from './callbackJobs';
+import libraries from './libraries';
export const apiRequests = [
'dependenciesRequest',
@@ -47,6 +48,7 @@ function mainReducer() {
}, apiRequests);
parts.callbackJobs = callbackJobs;
+ parts.libraries = libraries;
return combineReducers(parts);
}
diff --git a/dash/dash-renderer/src/types/callbacks.ts b/dash/dash-renderer/src/types/callbacks.ts
index 62fcb19d20..6a499cf1d1 100644
--- a/dash/dash-renderer/src/types/callbacks.ts
+++ b/dash/dash-renderer/src/types/callbacks.ts
@@ -1,3 +1,5 @@
+import {LibraryResource} from '../libraries/libraryTypes';
+
type CallbackId = string | {[key: string]: any};
export interface ICallbackDefinition {
@@ -103,4 +105,5 @@ export type CallbackResponseData = {
running?: CallbackResponse;
runningOff?: CallbackResponse;
cancel?: ICallbackProperty[];
+ resources: LibraryResource[];
};
diff --git a/dash/dash.py b/dash/dash.py
index 6de2507f9d..51aef43fcf 100644
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -404,6 +404,8 @@ def __init__( # pylint: disable=too-many-statements
add_log_handler=True,
hooks: Union[RendererHooks, None] = None,
routing_callback_inputs: Optional[Dict[str, Union[Input, State]]] = None,
+ dynamic_loading=True,
+ preloaded_libraries=None,
**obsolete,
):
_validate.check_obsolete(obsolete)
@@ -458,6 +460,8 @@ def __init__( # pylint: disable=too-many-statements
title=title,
update_title=update_title,
include_pages_meta=include_pages_meta,
+ dynamic_loading=dynamic_loading,
+ preloaded_libraries=preloaded_libraries or [],
)
self.config.set_read_only(
[
@@ -468,6 +472,8 @@ def __init__( # pylint: disable=too-many-statements
"serve_locally",
"compress",
"pages_folder",
+ "dynamic_loading",
+ "preloaded_libraries",
],
"Read-only: can only be set in the Dash constructor",
)
@@ -645,6 +651,7 @@ def _setup_routes(self):
self._add_url("_dash-update-component", self.dispatch, ["POST"])
self._add_url("_reload-hash", self.serve_reload_hash)
self._add_url("_favicon.ico", self._serve_default_favicon)
+ self._add_url("_dash-dist", self.serve_dist, methods=["POST"])
self._add_url("", self.index)
if jupyter_dash.active:
@@ -798,7 +805,17 @@ def serve_reload_hash(self):
}
)
- def _collect_and_register_resources(self, resources):
+ def serve_dist(self):
+ libraries = flask.request.get_json()
+ dists = []
+ for dist_type in ("_js_dist", "_css_dist"):
+ resources = ComponentRegistry.get_resources(dist_type, libraries)
+ srcs = self._collect_and_register_resources(resources, False)
+ for src in srcs:
+ dists.append(dict(type=dist_type, url=src))
+ return flask.jsonify(dists)
+
+ def _collect_and_register_resources(self, resources, include_async=True):
# now needs the app context.
# template in the necessary component suite JS bundles
# add the version number of the package as a query parameter
@@ -827,6 +844,8 @@ def _relative_url_path(relative_package_path="", namespace=""):
srcs = []
for resource in resources:
is_dynamic_resource = resource.get("dynamic", False)
+ is_async = resource.get("async") is not None
+ excluded = not include_async and is_async
if "relative_package_path" in resource:
paths = resource["relative_package_path"]
@@ -838,7 +857,7 @@ def _relative_url_path(relative_package_path="", namespace=""):
self.registered_paths[resource["namespace"]].add(rel_path)
- if not is_dynamic_resource:
+ if not is_dynamic_resource and not excluded:
srcs.append(
_relative_url_path(
relative_package_path=rel_path,
@@ -846,7 +865,7 @@ def _relative_url_path(relative_package_path="", namespace=""):
)
)
elif "external_url" in resource:
- if not is_dynamic_resource:
+ if not is_dynamic_resource and not excluded:
if isinstance(resource["external_url"], str):
srcs.append(resource["external_url"])
else:
@@ -873,7 +892,13 @@ def _relative_url_path(relative_package_path="", namespace=""):
def _generate_css_dist_html(self):
external_links = self.config.external_stylesheets
- links = self._collect_and_register_resources(self.css.get_all_css())
+
+ if self.config.dynamic_loading:
+ links = self._collect_and_register_resources(
+ self.css.get_library_css(self.config.preloaded_libraries)
+ )
+ else:
+ links = self._collect_and_register_resources(self.css.get_all_css())
return "\n".join(
[
@@ -908,21 +933,28 @@ def _generate_scripts_html(self):
self.scripts._resources._filter_resources(deps, dev_bundles=dev)
)
+ self.config.external_scripts
- + self._collect_and_register_resources(
+ )
+
+ if not self.config.dynamic_loading:
+ srcs += self._collect_and_register_resources(
self.scripts.get_all_scripts(dev_bundles=dev)
- + self.scripts._resources._filter_resources(
- _dash_renderer._js_dist, dev_bundles=dev
- )
- + self.scripts._resources._filter_resources(
- dcc._js_dist, dev_bundles=dev
- )
- + self.scripts._resources._filter_resources(
- html._js_dist, dev_bundles=dev
- )
- + self.scripts._resources._filter_resources(
- dash_table._js_dist, dev_bundles=dev
+ )
+ else:
+ srcs += self._collect_and_register_resources(
+ self.scripts.get_library_scripts(
+ self.config.preloaded_libraries, dev_bundles=dev
)
)
+
+ srcs += self._collect_and_register_resources(
+ self.scripts._resources._filter_resources(
+ _dash_renderer._js_dist, dev_bundles=dev
+ )
+ + self.scripts._resources._filter_resources(dcc._js_dist, dev_bundles=dev)
+ + self.scripts._resources._filter_resources(html._js_dist, dev_bundles=dev)
+ + self.scripts._resources._filter_resources(
+ dash_table._js_dist, dev_bundles=dev
+ )
)
self._inline_scripts.extend(_callback.GLOBAL_INLINE_SCRIPTS)
diff --git a/dash/development/base_component.py b/dash/development/base_component.py
index b51fc06634..0bc4a28c9c 100644
--- a/dash/development/base_component.py
+++ b/dash/development/base_component.py
@@ -20,10 +20,12 @@ class ComponentRegistry:
children_props = collections.defaultdict(dict)
@classmethod
- def get_resources(cls, resource_name):
+ def get_resources(cls, resource_name, includes=None):
resources = []
for module_name in cls.registry:
+ if includes is not None and module_name not in includes:
+ continue
module = sys.modules[module_name]
resources.extend(getattr(module, resource_name, []))
diff --git a/dash/resources.py b/dash/resources.py
index cae4a49470..2ac080d57a 100644
--- a/dash/resources.py
+++ b/dash/resources.py
@@ -88,6 +88,12 @@ def get_all_resources(self, dev_bundles=False):
return self._filter_resources(all_resources, dev_bundles)
+ def get_library_resources(self, libraries, dev_bundles=False):
+ lib_resources = ComponentRegistry.get_resources(self.resource_name, libraries)
+ all_resources = lib_resources + self._resources
+
+ return self._filter_resources(all_resources, dev_bundles)
+
# pylint: disable=too-few-public-methods
class _Config:
@@ -107,6 +113,9 @@ def append_css(self, stylesheet):
def get_all_css(self):
return self._resources.get_all_resources()
+ def get_library_css(self, libraries):
+ return self._resources.get_library_resources(libraries)
+
class Scripts:
def __init__(self, serve_locally, eager):
@@ -118,3 +127,6 @@ def append_script(self, script):
def get_all_scripts(self, dev_bundles=False):
return self._resources.get_all_resources(dev_bundles)
+
+ def get_library_scripts(self, libraries, dev_bundles=False):
+ return self._resources.get_library_resources(libraries, dev_bundles)
diff --git a/tests/integration/long_callback/app_page_cancel.py b/tests/integration/long_callback/app_page_cancel.py
index 5134ad7a14..d99ed6ac82 100644
--- a/tests/integration/long_callback/app_page_cancel.py
+++ b/tests/integration/long_callback/app_page_cancel.py
@@ -1,7 +1,5 @@
from dash import Dash, Input, Output, dcc, html, page_container, register_page
-import time
-
from tests.integration.long_callback.utils import get_long_callback_manager
long_callback_manager = get_long_callback_manager()
@@ -23,7 +21,7 @@
page_container,
]
)
-
+app.test_lock = lock = long_callback_manager.test_lock
register_page(
"one",
@@ -78,7 +76,8 @@ def on_click_no_cancel(_):
interval=300,
)
def on_click1(n_clicks):
- time.sleep(2)
+ with lock:
+ pass
return f"Click {n_clicks}"
@@ -97,7 +96,8 @@ def on_click1(n_clicks):
interval=300,
)
def on_click1(n_clicks):
- time.sleep(2)
+ with lock:
+ pass
return f"Click {n_clicks}"
diff --git a/tests/integration/long_callback/test_basic_long_callback016.py b/tests/integration/long_callback/test_basic_long_callback016.py
index 9b8f781ed2..e822a408e4 100644
--- a/tests/integration/long_callback/test_basic_long_callback016.py
+++ b/tests/integration/long_callback/test_basic_long_callback016.py
@@ -1,5 +1,4 @@
import sys
-import time
import pytest
@@ -12,32 +11,37 @@
def test_lcbc016_multi_page_cancel(dash_duo, manager):
with setup_long_callback_app(manager, "app_page_cancel") as app:
dash_duo.start_server(app)
- dash_duo.find_element("#start1").click()
- dash_duo.wait_for_text_to_equal("#progress1", "running")
- dash_duo.find_element("#shared_cancel").click()
- dash_duo.wait_for_text_to_equal("#progress1", "idle")
- time.sleep(2.1)
+
+ with app.test_lock:
+ dash_duo.find_element("#start1").click()
+ dash_duo.wait_for_text_to_equal("#progress1", "running")
+ dash_duo.find_element("#shared_cancel").click()
+ dash_duo.wait_for_text_to_equal("#progress1", "idle")
+
dash_duo.wait_for_text_to_equal("#output1", "initial")
- dash_duo.find_element("#start1").click()
- dash_duo.wait_for_text_to_equal("#progress1", "running")
- dash_duo.find_element("#cancel1").click()
- dash_duo.wait_for_text_to_equal("#progress1", "idle")
- time.sleep(2.1)
+ with app.test_lock:
+ dash_duo.find_element("#start1").click()
+ dash_duo.wait_for_text_to_equal("#progress1", "running")
+ dash_duo.find_element("#cancel1").click()
+ dash_duo.wait_for_text_to_equal("#progress1", "idle")
+
dash_duo.wait_for_text_to_equal("#output1", "initial")
dash_duo.server_url = dash_duo.server_url + "/2"
- dash_duo.find_element("#start2").click()
- dash_duo.wait_for_text_to_equal("#progress2", "running")
- dash_duo.find_element("#shared_cancel").click()
- dash_duo.wait_for_text_to_equal("#progress2", "idle")
- time.sleep(2.1)
+ with app.test_lock:
+ dash_duo.find_element("#start2").click()
+ dash_duo.wait_for_text_to_equal("#progress2", "running")
+ dash_duo.find_element("#shared_cancel").click()
+ dash_duo.wait_for_text_to_equal("#progress2", "idle")
+
dash_duo.wait_for_text_to_equal("#output2", "initial")
- dash_duo.find_element("#start2").click()
- dash_duo.wait_for_text_to_equal("#progress2", "running")
- dash_duo.find_element("#cancel2").click()
- dash_duo.wait_for_text_to_equal("#progress2", "idle")
- time.sleep(2.1)
+ with app.test_lock:
+ dash_duo.find_element("#start2").click()
+ dash_duo.wait_for_text_to_equal("#progress2", "running")
+ dash_duo.find_element("#cancel2").click()
+ dash_duo.wait_for_text_to_equal("#progress2", "idle")
+
dash_duo.wait_for_text_to_equal("#output2", "initial")
diff --git a/tests/integration/renderer/test_libraries.py b/tests/integration/renderer/test_libraries.py
new file mode 100644
index 0000000000..e7332c99d3
--- /dev/null
+++ b/tests/integration/renderer/test_libraries.py
@@ -0,0 +1,52 @@
+from dash import Dash, html, Input, Output
+import dash_test_components as dt
+import dash_generator_test_component_standard as dgs
+
+
+def test_rblib001_dynamic_loading(dash_duo):
+ app = Dash(__name__)
+
+ app.layout = html.Div(
+ [
+ html.Button("Insert", id="insert-btn"),
+ html.Div(id="output"),
+ dgs.MyStandardComponent(id="dgs"),
+ ]
+ )
+
+ @app.callback(
+ Output("output", "children"),
+ [Input("insert-btn", "n_clicks")],
+ prevent_initial_call=True,
+ )
+ def update_output(_):
+ import dash_generator_test_component_nested as dn
+
+ return [
+ dt.StyledComponent(value="Styled", id="styled"),
+ dn.MyNestedComponent(value="nested", id="nested"),
+ ]
+
+ dash_duo.start_server(app)
+
+ def assert_unloaded(namespace):
+ assert dash_duo.driver.execute_script(
+ f"return window['{namespace}'] === undefined"
+ )
+
+ def assert_loaded(namespace):
+ assert dash_duo.driver.execute_script(
+ f"return window['{namespace}'] !== undefined"
+ )
+
+ assert_unloaded(dt.package_name)
+ assert_unloaded("dash_generator_test_component_nested")
+ dash_duo.wait_for_element("#dgs")
+ assert_unloaded(dt.package_name)
+ assert_loaded(dgs.package_name)
+
+ dash_duo.wait_for_element("#insert-btn").click()
+
+ dash_duo.wait_for_element("#styled")
+ assert_loaded(dt.package_name)
+ assert_loaded("dash_generator_test_component_nested")
diff --git a/tests/integration/test_generation.py b/tests/integration/test_generation.py
index 1ed4f7ac78..b888c5b2cc 100644
--- a/tests/integration/test_generation.py
+++ b/tests/integration/test_generation.py
@@ -56,7 +56,13 @@ def test_gene001_simple_callback(dash_duo):
def test_gene002_arbitrary_resources(dash_duo):
app = Dash(__name__)
- app.layout = Div([Button("Click", id="btn"), Div(id="container")])
+ app.layout = Div(
+ [
+ Button("Click", id="btn"),
+ Div(id="container"),
+ MyStandardComponent(),
+ ]
+ )
@app.callback(Output("container", "children"), [Input("btn", "n_clicks")])
def update_container(n_clicks):