bug: client rects of any Ionic component in React is not measurable
See original GitHub issueI am trying to calculate the height of a Ionic react component, but it is not possible after the initial render is done. The common React techniques in use and all the common libraries that return measures of a component do not work with ionic and return a 0x0 size.
My suspect is that the Ionic components have an internal lifecycle that decides whether to render the component or not based on the hydration status and the stack position of the IonPage hosting the component.
However this makes it impossible to measure the size of a component, unless the ‘measure tool’ constantly polls for a size, which might destroy performances.
In the examples below, I have three different implementations:
- the first one is called useMeasure and is very similar to what is described in https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
- the second one is called useMeasureAlt and gets the height of the component using useLayoutEffect
- the third one id called useMeasureDelay and is a variant of the previous, where if the height is 0, it schedules a redraw (changing a state variable) until height > 0.
The first two methods do not return any height. The third returns a height after some re-renders.
IMHO the issue could be solved in two ways:
- implement on a onIonRendered(e) event, which triggers when the element is really rendered - and therefore the clientRect is available -
- when the component is rendered, invoke the ref function.
Thanks, Marco
https://codesandbox.io/embed/proud-glade-lrpc9?fontsize=14
function useMeasure<T extends HTMLElement>() : [(node: T) => void, number | null]{
const rect = React.useRef<ClientRect | DOMRect | null>(null)
const refCallback = React.useCallback ( (node: T) => {
rect.current = node.getBoundingClientRect();
console.log(`Measured: ${JSON.stringify(rect.current)}`)
},[]);
return [refCallback, rect.current && rect.current.height]
}
function useMeasureAlt<T extends HTMLElement>() : [React.RefObject<T>, number | null]{
const [rect, setRect] = React.useState<ClientRect | DOMRect | null>(null)
const ref = React.useRef<T>(null);
React.useLayoutEffect( () => {
if (ref.current) {
const newRect = ref.current.getBoundingClientRect();
if (!rect || rect.height !== newRect.height)
setRect(newRect);
console.log(`Measured in LayoutEffect: ${JSON.stringify(newRect)}`)
}
},)
return [ref, rect && rect.height]
}
function useMeasureWithDelay<T extends HTMLElement> () : [React.RefObject<T>, number | null] {
const ref = React.useRef<T>(null)
const [forceRedraw, setForceRedraw] = React.useState(1);
const rect = React.useRef<ClientRect | DOMRect | null>(null)
//this effect is hacky trick to force a re-render until IonGrid has done its duty...
React.useLayoutEffect( () => {
rect.current = ref.current && ref.current.getBoundingClientRect()
console.log(`Measured with delay: ${JSON.stringify(rect.current)}. forceRedraw: ${forceRedraw}`)
if (!ref.current) return;
if (!rect.current!.height && forceRedraw>0) {
setTimeout( ()=>setForceRedraw(v => v+1),1);
}
if (rect.current!.height) {
setForceRedraw(0);
}
},[forceRedraw, rect])
return [ref,rect.current && rect.current.height];
}
const MeasureTest : React.FC = () => {
const [ref, height] = useMeasure<HTMLIonRowElement>();
return <IonRow ref={ref}>
<IonCol><IonText>Measure test. Hello world. My height is {height}px</IonText></IonCol>
</IonRow>
}
const MeasureWithLayoutTest : React.FC = () => {
const [ref, height] = useMeasureAlt<HTMLIonRowElement>();
return <IonRow ref={ref}>
<IonCol><IonText>Layout test. Hello world. My height is {height}px</IonText></IonCol>
</IonRow>
}
const MeasureWithDelayTest : React.FC = () => {
const [ref, height] = useMeasureWithDelay<HTMLIonRowElement>();
return <IonRow ref={ref}>
<IonCol><IonText>Layout test. Hello world. My height is {height}px</IonText></IonCol>
</IonRow>
}
const App: React.FunctionComponent = () => {
return (
<IonApp>
<IonReactRouter>
<IonPage id="main">
<IonGrid>
<MeasureTest/>
<MeasureWithLayoutTest/>
<MeasureWithDelayTest/>
</IonGrid>
</IonPage>
</IonReactRouter>
</IonApp>
)}
export default App;
Issue Analytics
- State:
- Created 4 years ago
- Reactions:5
- Comments:7
Top GitHub Comments
Currently, I found a workaround that “works” using element-resize-detector It works like a charm, with the added bonus that if the size changes, you’ll get the new size, but I still think it would be nice if stencil components behave nicely 😄
Thanks for the issue! This issue is being closed due to inactivity. If this is still an issue with the latest version of Ionic, please create a new issue and ensure the template is fully filled out.
Thank you for using Ionic!