Skip to content

Commit

Permalink
Merge pull request #2762 from plotly/feat/dynamic-loading
Browse files Browse the repository at this point in the history
Dynamic loading of component libraries.
  • Loading branch information
T4rk1n authored Feb 29, 2024
2 parents ba22c4d + 8e4b4fe commit 330a2c4
Show file tree
Hide file tree
Showing 24 changed files with 666 additions and 113 deletions.
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), []);

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

0 comments on commit 330a2c4

Please sign in to comment.