Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic loading of component libraries. #2762

Merged
merged 20 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ StyledComponent.propTypes = {
/**
* The style
*/
style: PropTypes.shape,
style: PropTypes.object,

/**
* The value to display
Expand All @@ -27,4 +27,4 @@ StyledComponent.defaultProps = {
value: ''
};

export default StyledComponent;
export default StyledComponent;
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 34 additions & 10 deletions dash/dash-renderer/src/APIController.react.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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({});

Expand All @@ -46,6 +53,10 @@ const UnconnectedContainer = props => {
if (!events.current) {
events.current = new EventEmitter();
}

const [libraryReady, setLibraryReady] = useState(false);
const onLibraryReady = useCallback(() => setLibraryReady(true), []);
alexcjohnson marked this conversation as resolved.
Show resolved Hide resolved

const renderedTree = useRef(false);

const propsRef = useRef({});
Expand All @@ -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) {
Expand Down Expand Up @@ -117,14 +130,23 @@ const UnconnectedContainer = props => {
content = <div className='_dash-loading'>Loading...</div>;
}

return config && config.ui === true ? (
<GlobalErrorContainer>{content}</GlobalErrorContainer>
) : (
content
return (
<LibraryManager
requests_pathname_prefix={config.requests_pathname_prefix}
onReady={onLibraryReady}
ready={libraryReady}
layout={layoutRequest && layoutRequest.content}
>
{config && config.ui === true ? (
<GlobalErrorContainer>{content}</GlobalErrorContainer>
) : (
content
)}
</LibraryManager>
);
};

function storeEffect(props, events, setErrorLoading) {
function storeEffect(props, events, setErrorLoading, libraryReady) {
const {
appLifecycle,
dependenciesRequest,
Expand All @@ -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);
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
30 changes: 30 additions & 0 deletions dash/dash-renderer/src/CheckedComponent.react.js
Original file line number Diff line number Diff line change
@@ -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
};
68 changes: 14 additions & 54 deletions dash/dash-renderer/src/TreeContainer.js
Original file line number Diff line number Diff line change
@@ -1,79 +1,44 @@
import React, {Component, memo, useContext} from 'react';
import PropTypes from 'prop-types';
import Registry from './registry';
import {propTypeErrorHandler} from './exceptions';
import {
addIndex,
assoc,
assocPath,
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,
getLoadingState,
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' &&
Expand Down Expand Up @@ -250,8 +215,6 @@ class BaseTreeContainer extends Component {
}
validateComponent(_dashprivate_layout);

const element = Registry.resolve(_dashprivate_layout);

// Hydrate components props
const childrenProps = pathOr(
[],
Expand Down Expand Up @@ -455,17 +418,14 @@ class BaseTreeContainer extends Component {
dispatch={_dashprivate_dispatch}
error={_dashprivate_error}
>
{_dashprivate_config.props_check ? (
<CheckedComponent
children={children}
element={element}
props={props}
extraProps={extraProps}
type={_dashprivate_layout.type}
/>
) : (
createElement(element, props, extraProps, children)
)}
<LibraryComponent
children={children}
type={_dashprivate_layout.type}
namespace={_dashprivate_layout.namespace}
props={props}
extraProps={extraProps}
props_check={_dashprivate_config.props_check}
/>
</ComponentErrorBoundary>
);
}
Expand Down
43 changes: 40 additions & 3 deletions dash/dash-renderer/src/actions/callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<IBlockedCallback[]>(
CallbackActionType.AddBlocked
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
10 changes: 10 additions & 0 deletions dash/dash-renderer/src/actions/libraries.ts
Original file line number Diff line number Diff line change
@@ -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);
Loading