reactjsiframereact-hooksiframe-resizer

Issue with iframe-resizer getPageInfo callback and React


I'm having an issue with iframe-resizer and react when using hooks.
I'm not sure if I'm using getPageInfo correctly with useEffect.

Background

I have an element in the iframe whose position is dependant on how far the they have scrolled on the page.
Essentially these elements should follow the user's scroll position, and sticky and fixed are not working due to the iframe-resizer library.

The user may do the following things that should trigger an HTML element style to be recalculated

  1. The user changes their screen size (orientation, etc.)
    • e.g elementBreakpoint, this should trigger the effect and recalculate the positions
  2. The user clicks a button in the UI to "open" the element (i.e. to fill the screen)
The parent frame is set up like this:
iFrameResize({
  checkOrigin: false,
  // Height calculation
  heightCalculationMethod: 'bodyOffset',
  // To use "taggedElement", ensure that `<div data-iframe-height></div>` exists in the iframe's DOM
  // heightCalculationMethod: "taggedElement",
  // Interval, default 32(ms)
  // Interval set to 1/4 the default, in an effort to reduce the slow transitions
  interval: 8,
  // Tolerance, default 0(px)
  // Tolerance allows a change of N pixels before triggering a resize event
  tolerance: 10,
  // Calls once the iframe is set up
  initCallback: function () {
    // console.log("iframe initCallback");
  },
  // Callback for when the iFrame is resized
  resizedCallback: iframeResizeCallback,
  // Callback when a message is received from the child iframe to the parent page
  messageCallback: function (received) {
    // console.log("message", received);
  }
}, iframeElement);

The child frame is initialised like this:
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/3.6.5/iframeResizer.contentWindow.min.js"></script>

In the below code I do the following:

  1. pass a callback to getPageInfo inside a useEffect
  2. calculate the positions of the elements
  3. save the parent page scroll positions to Redux (for other functions to use where needed)
  4. save the calculated positions to Redux (for other elements to use in the style prop)

Which effectively looks like:

useEffect(() => {
  window.parentIFrame.getPageInfo((positions) => {
    // Handle timeouts to stop the Redux state being set too often
    if (getPageInfoTimeout.current) {
      clearTimeout(getPageInfoTimeout.current);
    }
    getPageInfoTimeout.current =  setTimeout(() => {
      // calculate styles and set to Redux state
    }, 50)
  });
  return () => {
    // cleanup `window.parentIFrame.getPageInfo`
    window.parentIFrame.getPageInfo(false);
  }
}, [
  // some dependencies and UI breakpoints
])

Am I doing something unwise/stupid with the order of the code?

Attempt 1 - useEffect

This works for action2, but fails for action 1

// timeout in ms
const UPDATE_TIMEOUT = 50;

export const useUpdateSidebarPositions: UseUpdateSidebarPositions = () => {
  // Redux - State
  // Check if the element is open or not
  // used for the style calculation
  const elementOpen = useSelector(
    (store: ReduxStore) => store.elementOpen
  );

  // if the page is in an iframe, the effect will run
  const inIframe = useSelector(
    (store: ReduxStore) => store.inIframe
  );
  // below is used for the style calculation
  const headerHeight = useSelector(
    (store: ReduxStore) => store.scrollingPosition.header
  );
  const footerHeight = useSelector(
    (store: ReduxStore) => store.scrollingPosition.footer
  );
  const parentHeight = useSelector(
    (store: ReduxStore) => store.scrollingPosition.parentHeight
  );
  const parentWidth = useSelector(
    (store: ReduxStore) => store.scrollingPosition.parentWidth
  );
  const scrollbarWidth = useSelector(
    (store: ReduxStore) => store.scrollingPosition.scrollbarWidth
  );

  // Redux - Actions
  const dispatch = useDispatch();
  const setElementPositions: ReduxSetElementPositions = useCallback(
    (data) => dispatch(uiActions.setElementPositions(data)),
    [dispatch]
  );
  const setScrollingPosition: ReduxSetScrollingPosition = useCallback(
    (data) => dispatch(uiActions.setScrollingPosition(data)),
    [dispatch]
  );

  // Timeouts
  const scrollPositionTimeout = useRef<TimeoutRef>(null);
  const getPageInfoTimeout = useRef<TimeoutRef>(null);

  const elementBreakpoint = useMedia({ maxWidth: "1024px" });

  // this delays the starting of the effect, I think there may be
  // some race conditions when the page starts loading
  const delayComplete = useDelayComplete();

  useEffect(() => {
    if (inIframe && window.parentIFrame && delayComplete) {
      // Add listener
      // ---- point 1 ---- //
      window.parentIFrame.getPageInfo(
        ({
          clientHeight,
          clientWidth,
          iframeHeight,
          iframeWidth,
          // offsetLeft,
          // offsetTop,
          // scrollLeft,
          scrollTop,
        }) => {
          if (scrollPositionTimeout.current) {
            clearTimeout(scrollPositionTimeout.current);
          }
          if (getPageInfoTimeout.current) {
            clearTimeout(getPageInfoTimeout.current);
          }
          getPageInfoTimeout.current = setTimeout(() => {

            // Set up scroll positions
            const scrollPositions: Parameters<
              typeof calculateSideBarPosition
            >[0] = {
              header: headerHeight,
              footer: footerHeight,
              iframeHeight: iframeHeight,
              iframeWidth: iframeWidth,
              parentHeight: clientHeight,
              parentWidth: clientWidth,
              yScroll: scrollTop,
            };
            // ---- point 2 ---- //
            // Calculate position values
            const calculatedPositions =
              calculatePosition(scrollPositions);
            // Set styles to Redux
            // ---- point 4 ---- //
            setElementPositions(calculatedPositions);

            // Set scroll positions for other components to use
            // ---- point 3 ---- //
            scrollPositionTimeout.current = setTimeout(
              () => setScrollingPosition(scrollPositions),
              TIMEOUTS.interfaceElements.updateScrollPositions
            );
          }, UPDATE_TIMEOUT);
        }
      );

      return () => {
        if (scrollPositionTimeout.current) {
          clearTimeout(scrollPositionTimeout.current);
        }
        window.parentIFrame.getPageInfo(false);
        if (getPageInfoTimeout.current) {
          clearTimeout(getPageInfoTimeout.current);
        }
      };
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    delayComplete,
    inIframe,
    elementBreakpoint,
    elementOpen,
    window.parentIFrame,
    headerHeight,
    footerHeight,
    parentHeight,
    parentWidth,
    scrollbarWidth,
    setElementPositions,
    setScrollingPosition,
  ]);
};

Attempt 2 - useLayoutEffect

This currently fails on both actions, if the user has "opened" the element (which triggers a scroll blocking function) and the user somehow triggers a scroll event, the element is recalculated correctly, and this seems really wrong...
Note: there are no direct DOM calculations anymore, but I am using document.activeElement, this was deleted as it's not relevant to the code example

export const useUpdateSidebarPositions: UseUpdateSidebarPositions = () => {
  // Redux - State
  // ...
  // same Redux State & Actions as before
  // ...

  useLayoutEffect(() => {
    if (inIframe && window.parentIFrame) {
      // Add listener
      // ---- point 1 ---- //
      window.parentIFrame.getPageInfo(
        async ({
          clientHeight,
          clientWidth,
          iframeHeight,
          iframeWidth,
          // offsetLeft,
          // offsetTop,
          // scrollLeft,
          scrollTop,
        }) => {
          if (!delayComplete) {
            return;
          }
          // Set up scroll positions
          const scrollPositions: Parameters<
            typeof calculateSideBarPosition
          >[0] = {
            header: headerHeight,
            footer: footerHeight,
            iframeHeight: iframeHeight,
            iframeWidth: iframeWidth,
            parentHeight: clientHeight,
            parentWidth: clientWidth,
            yScroll: scrollTop,
          };


          // Set scroll positions for other components to use
          // e.g: Fixed Header and Modals
          if (scrollPositionTimeout.current) {
            clearTimeout(scrollPositionTimeout.current);
          }
          // ---- point 3 ---- //
          scrollPositionTimeout.current = setTimeout(
            () => setScrollingPosition(scrollPositions),
            TIMEOUTS.interfaceElements.updateScrollPositions
          );

          if (getPageInfoTimeout.current) {
            clearTimeout(getPageInfoTimeout.current);
          }
          getPageInfoTimeout.current =  setTimeout(() => {
            // ---- point 2 ---- //
            // Calculate position values
            const calculatedPositions =
              calculateSideBarPosition(scrollPositions);

            // Set styles to Redux
            // ---- point 4 ---- //
            setElementPositions(calculatedPositions);
          }, UPDATE_TIMEOUT);
        }
      );

      return () => {
        // Clear listener
        window.parentIFrame.getPageInfo(false);
        // Clear Timeouts on cleanup
        if (scrollPositionTimeout.current) {
          clearTimeout(scrollPositionTimeout.current);
        }
        window.parentIFrame.getPageInfo(false);
        if (getPageInfoTimeout.current) {
          clearTimeout(getPageInfoTimeout.current);
        }
      };
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    delayComplete,
    inIframe,
    elementBreakpoint,
    elementOpen,
    window.parentIFrame,
    headerHeight,
    footerHeight,
    parentHeight,
    parentWidth,
    scrollbarWidth,
    setElementPositions,
    setScrollingPosition,
  ]);
};


Solution

  • A few thoughts:

    I would suggest using middleware to throttle Redux actions. I once did a talk on Redux performance tuning which you might find helpful.

    https://github.com/davidjbradshaw/redux-performance-talk

    I am not a fan of useDispatch(), it provides no benefits over using the older connect()() high order function method, whilst passing on responsibility and complexity to you for caching the component.

    I’m a bit unsure why you’re using interval. It is really only there for supporting IE. Setting it to 8 means you’re checking everything twice per screen refresh.

    Beyond that it is very hard to say much without seeing a working example. The question really is the data from getPageInfo ending up in the redux store. If so your issue is somewhere else in your code.

    Btw Iframe Resizer 5 was released last week.