Hi Lech,
Thanks for getting back to me and apologies for the delayed response. Totally appreciate the architecture of the library and what's going on in the render, I wasn;t sure if you had built any part loading hooks into it that we might be able to utilise in order to chunk load things. Can't remember the name of it off the top of my head but js does have the ability to queue parts to the stack so it can appear to be more responsive when running these sorts of processes. Although it would make sense you don't as my experience of that is it's quite hairy and would require a lot of thought and extra code to handle. As I said was just trying to see if there was anything in there already before we embark on trying to solve the problem some other way.
So on that, I thought I'd post our solution to share with others in the same boat. Luckily it actually worked out quite nicely, I'll preface by saying we built this solely to solve the problem in react. Hopefully people using other frameworks can take something from it.
The main premise is to somehow offload the loading from our main applications callstack, so the obvious approach was to have the editor load in a different browser thread, so effectively it's own page or iframe. Howevre, we wanted to still maintain the react component tree inside our app with the editor, so for us just building a vanailla html page for the editor and wrapping it with some sort of iframe api wasn't going to work as out react app needed to pass down alot of context in the form of the apollo client and state etc for manage the data inside the report.
We were able to keep our initial editor and viewer react components but wrap them inside an iframe using react portals. All credit really goes to react for providing this sort of functionality for a react tree to span an iframe through portals tbh.
We have used the react iframe libray 'react-frame-component', just because it already handled all the nuances of portal iframes in react and provided an easy interface for the iframe api. But there's no reason why you couldn't do this with vanilla iframes, react and js.
So below is the Stimulsoft loader wrapper component we use. note- I shared a cut down implementation of the editor component to show the use of the iframe 'window' variable/hook which you need to use to access the Stimuloft class rather than the global window, apart from that, you can treat the iframe child components as though they were just being directly rendered by this wrapper instead of the iframe.
The iframe library we've used provides a convienient componentDidUpdate property which is tied into the underlying iframe api and listens for when the iframe has finished loading. In this case (and why I was saying luckily above) that happens to be once Stimulsoft has rendered. So we use this hook to turn off our loader. What we also found was that loading stimulsoft inside it's own browser process also drastically increased the load performance, so we had a twofold effect, we got a nice loading UX that didn't hang becuase it's running in a different thread through the iframe, and it loads faster.
Hope that helps anyone else facing similar challenges
StimulsoftLoader.js
Code: Select all
import React, { useCallback, useState } from 'react'
import Frame, { useFrame } from 'react-frame-component'
import FuseLoading from '@fuse/core/FuseLoading'
import { makeStyles } from '@material-ui/core/styles'
import StimulsoftViewer from './StimulsoftViewer'
import StimulsoftEditor from './StiimulsoftEditor'
const useStyles = makeStyles(theme => ({
iframeWrap: {
height: 'calc(100% + 50px)',
width: '100%'
},
}))
const StimulsoftLoader = React.memo(({
dashboardMode,
content,
editMode
}) => {
const { window } = useFrame()
const [saving, setSaving] = useState()
const onSaveReport = useCallback(async (report, type) => {
//...do something here
}, [])
const onChangeReport = useCallback(({ report }, designer) => {
report = report.saveToJsonString()
onSaveReport(report, 'autosave')
}, [onSaveReport])
return (
<>
<StimulsoftEditor
key={`${content.id}-editor`}
dashboardMode={dashboardMode}
onSaveReport={onSaveReport}
onChangeReport={onChangeReport}
reportContent={content?.report}
hide={!editMode}
/>
<StimulsoftViewer
key={`${content.id}-viewer`}
reportContent={content?.report}
dashboardMode={dashboardMode}
saving={saving}
hide={editMode}
/>
</>
)
})
const StimualsoftWrapper = props => {
const classes = useStyles()
const [loading, setLoading] = useState(true)
return (
<div className={classes.iframeWrap} >
{loading && <FuseLoading classNames="h-full w-full" />}
<Frame
style={{width:"100%", height:"100%"}}
// contentDidMount={}
contentDidUpdate={() => setLoading()}
initialContent={`
<!DOCTYPE html>
<html>
<head>
<link href="https://unpkg.com/browse/stimulsoft-dashboards-js@2023.2.5/Css/stimulsoft.viewer.office2013.whiteblue.css" rel="stylesheet"/>
<link href="https://unpkg.com/browse/stimulsoft-dashboards-js@2023.2.5/Css/stimulsoft.designer.office2013.whiteblue.css" rel="stylesheet"/>
<script src="https://unpkg.com/stimulsoft-reports-js/Scripts/stimulsoft.reports.js" type="text/javascript"></script>
<script src="https://unpkg.com/stimulsoft-dashboards-js/Scripts/stimulsoft.dashboards.js" type="text/javascript"></script>
<script src="https://unpkg.com/stimulsoft-reports-js/Scripts/stimulsoft.viewer.js" type="text/javascript"></script>
<script src="https://unpkg.com/stimulsoft-reports-js/Scripts/stimulsoft.designer.js" type="text/javascript"></script>
<script src="https://unpkg.com/stimulsoft-reports-js/Scripts/stimulsoft.blockly.editor.js" type="text/javascript"></script>
<style>
.frame-content {
height: 100%;
}
html {
height: 100%;
}
body {
height: calc(100% - 12px)
}
.wrapper {
height: 100%;
}
#designer, #viewer {
width: 100%;
height: 100%;
}
#StiViewer {
height: calc(100vh - 8px) !important;
}
#StiDesigner {
height: 100vh;
}
// .stiJsViewerReportPanel {
// padding-top: 36px;
// }
</style>
<script>
Stimulsoft.Base.StiLicense.key = "<your license key>";
</script>
</head>
<body>
<div class="wrapper" >
</div>
</body>
</html>
`}
>
<StimulsoftLoader {...props} />
</Frame>
</div>
)
}
export default StimualsoftWrapper
Editor.js
Code: Select all
import React, { useState, useLayoutEffect, useEffect, useCallback, useRef } from 'react'
import { useFrame } from 'react-frame-component'
const StimulsoftEditor = ({
reportContent,
onSaveReport,
saving,
dashboardMode,
hide
}) => {
const { document, window } = useFrame()
const reportContentRef = useRef()
const designerContainerRef = useRef()
const designerLoadedRef = useRef()
const designerRef = useRef()
const [designer, setDesigner] = useState()
const [report, setReport] = useState()
const [loaded, setLoaded] = useState()
const generateReport = useCallback((content) => {
let report
if (dashboardMode) {
report = window.Stimulsoft.Report.StiReport.createNewDashboard()
} else {
report = window.Stimulsoft.Report.StiReport.createNewReport()
}
if (content) {
report.load(content)
}
return report
}, [dashboardMode])
useEffect(() => {
if (!hide) return
if (report && isEqual(reportContentRef.current, reportContent)) return
const newReport = generateReport(reportContent)
reportContentRef.current = reportContent
setReport(newReport)
}, [reportContent, reportContentRef, generateReport, report, setReport, hide])
useEffect(() => {
if (!report || designer || hide) return
setLoaded(false)
const options = new window.Stimulsoft.Designer.StiDesignerOptions()
const newDesigner = new window.Stimulsoft.Designer.StiDesigner(options, "StiDesigner", false)
newDesigner.onSaveReport = onSaveReport
newDesigner.report = report
designerRef.current = newDesigner
setDesigner(newDesigner)
}, [
report,
designer,
designerRef,
setDesigner,
onSaveReport,
hide,
setLoaded,
dashboardMode,
])
useLayoutEffect(() => {
if (loaded || hide) return
if (designer && designerContainerRef.current && !designerLoadedRef.current) {
designer?.renderHtml("designer")
designerLoadedRef.current = true
const designerElement = document.getElementById("StiDesigner")
if (designerElement) {
designerElement.style.height = '100%'
setLoaded(designerElement)
}
}
}, [designer, designerContainerRef, designerLoadedRef, loaded, setLoaded])
return (<div id="designer" ref={designerContainerRef} hidden={hide} ></div>)
}
export default React.memo(StimulsoftEditor)