I’m having an issue with iframe-resizer
and react
when using hooks.
I’m not sure if I’m using getPageInfo
in the correct manner 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
- The user changes their screen size (orientation, etc.)
- e.g
elementBreakpoint
, this should trigger the effect and recalculate the positions
- e.g
- 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:
- pass a callback to
getPageInfo
inside auseEffect
- calculate the positions of the elements
- save the parent page scroll positions to Redux (for other functions to use where needed)
- 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,
]);
};