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
.
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
elementBreakpoint
, this should trigger the effect and recalculate the positionsiFrameResize({
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);
<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:
getPageInfo
inside a useEffect
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?
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,
]);
};
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,
]);
};
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.