iTwin/appui

Widget content effects fire before it is actually rendered in widget

Closed this issue ยท 3 comments

Describe the bug

This is more noticeable when I navigate between frontstages - to reproduce issue widget in question should not be present in another frontstage because it needs to be unmounted first.

When rendering react component as widget contents, all effects are being fired before widget is actually visible in widget DOM. When observing it with breakpoints at useEffect/useLayoutEffect I can only see this when under highlighted element I expect to see rendered content:
image

This messes up layout in cases where effects were making changes to DOM or when it requires DOM to make calculations. I investigated further and used MutationObserver to confirm that widget content is actually being appended after effects fire (ContentRenderer.tsx):
image


Which is called because of (FrontstageDef.tsx):
image

To Reproduce

  1. Open test app with at least 2 frontstages, one fronstage has a widget that has useEffect in it.
  2. Set breakpoint for effect and navigate between frontstages.
  3. Inspect elements tree and notice widget is not actually placed inside DOM yet even though effect fired already.

Expected Behavior

DOM is already rendered before effects fire.

Screenshots

No response

Desktop (please complete the applicable information)

  • OS: Windows
  • Browser: Electron
  • Version: 25.8.1 (npm version)
  • iTwin.js AppUI Version: 4.10.0
  • iTwin.js Version: 4.4.3

Additional context

No response

That is correct, we are reparenting widget content DOM node to display widget content in different parts of the UI (i.e. different panel sections or floating widgets).
React element tree is stable per frontstage to prevent re-mounts when moving/docking/merging widgets in the UI.
Widget react nodes are loaded first and are portaled to a detached DOM element (which is what you are observing).
I.e. widget rendered w/ WidgetState.Hidden will run all React lifecycle methods (like useEffect, but the DOM element will not be added to the page).

We have a custom hook useTransientState that you can use to save/restore DOM properties that are not persisted when element parent is changed (like scroll position).
The function onSave is called before removing widget content DOM node and onRestore is called after appending the DOM node.

I think you could utilize useTransientState hook for your use case as well (instead of doing your calculations in useEffect, do them in onRestore). Would that work for you?

Could you describe a little more your use-case, what calculation are you doing and what are you trying to accomplish?

@GerardasB This info was useful to me, I utilized useTransientState hook's onSave and onRestore callback parameters to make sure I am doing logic in a rendered widget. Our use case: after component mount we are using React.useLayoutEffect to adjust position: absolute element's coordinates by changing left style property - it is calculated from DOM. Before widget is mounted in dom we calculate wrong left value then after it is mounted it seems sizing is different, but old left value is still in place.

While this fix resolved this issue for me it seems onRestore is not being called when popping out widget. Don't know if its intentional or not, just something I noticed.

Yeah, widget content is currently re-mounted when popout is opened. I think we should still be calling onSave/onRestore when popout is opened/closed.
Alternatively, we have this issue #524 for which the plan is to put #606 changes under a preview feature flag.