loading the editor blocks the callstack

Stimulsoft Reports.JS discussion
Post Reply
fridaystreet
Posts: 20
Joined: Thu Jun 08, 2023 11:40 pm

loading the editor blocks the callstack

Post by fridaystreet »

Is there anyway to load the editor without blocking the callstack in react?

everything is fine until you call designer.renderHtml("designer")

Then the whole application hangs until that finsihes. Been trying to show a loader while it loads in background, but the load displays and is just hung. The tooltip on the button which I have in a higher component to toggle between displaying the editor and viewer is even hangs and stays on while the editor renders. It happens the first time the editor is loaded into a component.

I am loading the main editor js files in the index.html, but the editor itself isn't loaded until the editor component mounts.

I've tried everything. Short of loading an skelton ediutor into a hidden div in the docroot or something when the actual page loads and then trying to copy/clone that and inject it into my react component, which I'm pretty dubious would even work, any ideas on how to do this would be great.

I really don't mind about the loading time, but not being able to actaully present a decent loading experience while it loads is causing really bad UX

Any assistance appreciated.

CHeers
Paul
Lech Kulikowski
Posts: 6294
Joined: Tue Mar 20, 2018 5:34 am

Re: loading the editor blocks the callstack

Post by Lech Kulikowski »

Hello,

The component is large and full of many divs. Consider that renderhtml is a continuation of script loading.
It is not possible to create elements in parts because of the complexity of the implementation
And since this is a pure js component, it makes no sense to create rendered html as well, threads will be blocked while the component is rendered.

If you have any ideas to speed it up, we are always ready to consider suggestions.

Thank you.
fridaystreet
Posts: 20
Joined: Thu Jun 08, 2023 11:40 pm

Re: loading the editor blocks the callstack

Post by fridaystreet »

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)
Lech Kulikowski
Posts: 6294
Joined: Tue Mar 20, 2018 5:34 am

Re: loading the editor blocks the callstack

Post by Lech Kulikowski »

Hello,

Thank you for the information.
Post Reply