Receive dimensions of element via getBoundingClientRect in React - reactjs

I would like to find out the dimensions of a DOM element in a reliable way.
My consideration was to use getBoundingClientRect for this.
const elementRef = useRef<HTMLUListElement | null>(null);
const [dimensions, setDimensions] = useState<DOMRect | undefined>();
const element: HTMLUListElement | null = elementRef.current;
/**
* Update dimensions state.
*/
const updateDimensions = useCallback(() => {
if (!element) return;
setDimensions(element.getBoundingClientRect());
}, [element, setDimensions]);
/**
* Effect hook to receive dimensions changes.
*/
useEffect(() => {
if (element) {
updateDimensions();
}
}, [element, updateDimensions]);
The problem with this approach is that useEffect only reacts to elementRef.current and not to the rect changes. Apparently the drawing of the component in the browser is not finished yet, because the dimensions are always 0.
If I do the whole thing outside the useEffect then it works. In the console I see 2 times values with 0 and then the correct dimensions.
With useEffect:
Outside of useEffect:
However, I would like to save the dimensions in the state and for this I need the useEffect.
How can I achieve this with getBoundingClientRect or is there another way to do this?
SOLUTION
It was not a problem of react it self. It is a bug in ionic V5 react.
Normally you can do it in this way:
https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
But here is an issue:
https://github.com/ionic-team/ionic/issues/19770
My solution is to use: https://github.com/que-etc/resize-observer-polyfill
import { RefCallback, useCallback, useState } from 'react';
import ResizeObserver from 'resize-observer-polyfill';
export function useDimensions(): [RefCallback<HTMLElement | null>, DOMRect | undefined] {
const [dimensions, setDimensions] = useState<DOMRect | undefined>();
const ref: RefCallback<HTMLElement | null> = useCallback((node: HTMLElement | null) => {
if (node) {
setDimensions(node.getBoundingClientRect().toJSON());
const resizeObserver = new ResizeObserver((entries) => {
if (entries.length) {
setDimensions(entries[0].target.getBoundingClientRect().toJSON());
}
});
resizeObserver.observe(node);
return () => {
resizeObserver.unobserve(node);
resizeObserver.disconnect();
};
}
}, []);
return [ref, dimensions];
}

you can use a callBackRef to achieve desired behaviour:
import React, { useCallback, useState } from "react";
export default function App() {
const [dimensions, setDimensions] = useState(null);
const callBackRef = useCallback(domNode => {
if (domNode) {
setDimensions(domNode.getBoundingClientRect());
}
}, []);
return (
<div>
<h1 ref={callBackRef}>Measure me</h1>
</div>
);
}`

If performance is a concern of yours, then on top of using the ResizeObserver I would use this library instead to avoid layout trashing:
https://github.com/ZeeCoder/use-resize-observer
Note that it doesn not use getBoundingClientReact() for this particular reason:
https://gist.github.com/paulirish/5d52fb081b3570c81e3a

Related

React: Trigger a function when a child asynchronously updates its DOM after rendering

Within ParentComponent, I render a chart (ResponsiveLine). I have a function (calculateHeight) calculating the height of some DOM elements of the chart.
To work fine, my function calculateHeight have to be triggered once the chart ResponsiveLine is rendered.
Here's my issue: useEffect will trigger before the child is done rendering, so I can't calculate the size of the DOM elements of the chart.
How to trigger my function calculateHeight once the chart ResponsiveLine is done rendering?
Here's a simplified code
const ParentComponent = () => {
const myref = useRef(null);
const [marginBottom, setMarginBottom] = useState(60);
useEffect(() => {
setMarginBottom(calculateHeight(myref));
});
return (
<div ref={myref}>
<ResponsiveLine marginBottom={marginBottom}/>
</div>)
}
EDIT
I can't edit the child ResponsiveLine, it's from a library
You can use the ResizeObserver API to track changes to the dimensions of the box of the div via its ref (specifically the height, which is the block size dimension for content which is in a language with a horizontal writing system like English). I won't go into the details of how the API works: you can read about it at the MDN link above.
The ResponsiveLine aspect of your question doesn't seem relevant except that it's a component you don't control and might change its state asynchronously. In the code snippet demonstration below, I've created a Child component that changes its height after 2 seconds to simulate the same idea.
Code in the TypeScript playground
<div id="root"></div><script src="https://unpkg.com/react#18.2.0/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#18.2.0/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.18.5/babel.min.js"></script><script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
<script type="text/babel" data-type="module" data-presets="tsx,react">
// import ReactDOM from 'react-dom/client';
// import {useEffect, useRef, useState, type ReactElement} from 'react';
// This Stack Overflow snippet demo uses UMD modules instead of the above import statments
const {useEffect, useRef, useState} = React;
// You didn't show this function, so I don't know what it does.
// Here's something in place of it:
function calculateHeight (element: Element): number {
return element.getBoundingClientRect().height;
}
function Child (): ReactElement {
const [style, setStyle] = useState<React.CSSProperties>({
border: '1px solid blue',
height: 50,
});
useEffect(() => {
// Change the height of the child element after 2 seconds
setTimeout(() => setStyle(style => ({...style, height: 150})), 2e3);
}, []);
return (<div {...{style}}>Child</div>);
}
function Parent (): ReactElement {
const ref = useRef<HTMLDivElement>(null);
const [marginBottom, setMarginBottom] = useState(60);
useEffect(() => {
if (!ref.current) return;
let lastBlockSize = 0;
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
if (!(entry.borderBoxSize && entry.borderBoxSize.length > 0)) continue;
// #ts-expect-error
const [{blockSize}] = entry.borderBoxSize;
if (blockSize === lastBlockSize) continue;
setMarginBottom(calculateHeight(entry.target));
lastBlockSize = blockSize;
}
});
observer.observe(ref.current, {box: 'border-box'});
return () => observer.disconnect();
}, []);
return (
<div {...{ref}}>
<div>height: {marginBottom}px</div>
<Child />
</div>
);
}
const reactRoot = ReactDOM.createRoot(document.getElementById('root')!);
reactRoot.render(<Parent />);
</script>
You said,
Here's my issue: useEffect will trigger before the child is done rendering, so I can't calculate the size of the DOM elements of the chart.
However, parent useEffect does not do that, It fires only after all the children are mounted and their useEffects are fired.
The value of myref is stored in myref.current So your useEffect should be
useEffect(() => {
setMarginBottom(calculateHeight(myref.current));
});
Why don't you send a function to the child component that is called from the useEffect of the child component.
const ParentComponent = () => {
const myref = useRef(null);
const [marginBottom, setMarginBottom] = useState(60);
someFunction = () => {
setMarginBottom(calculateHeight(myref));
}
return (
<div ref={myref}>
<ResponsiveLine func={someFunction} marginBottom={marginBottom}/>
</div>)
}
// CHILD COMPONENT
const ChildComponent = ({func, marginBotton}) => {
const [marginBottom, setMarginBottom] = useState(60);
useEffect(() => {
func();
}, []);
return <div></div>
}

Run React's useEffect at random intervals

I am new to React and ThreeJs. I am using react-three-fiber to animate a 3d model.
I have generated a React component that uses `useEffect to trigger some animations.
This code runs in an infinite loop it seems; I would like for the animation to run once, pause for a random number of seconds between 1 and 9, and then for the animation to run again.
import React, { useRef, useEffect } from 'react'
import { useGLTF, useAnimations } from '#react-three/drei'
export default function Model({ ...props }) {
const group = useRef()
const { nodes, materials, animations } = useGLTF('/blob.glb')
const { actions } = useAnimations(animations, group)
useEffect(() => {
console.log(actions)
actions.someAction.play()
});
return (
<group ref={group} {...props} dispose={null}>
<group position={[0.16, 0.21, 0]} scale={[1.13, 0.79, 1.13]}>
<mesh
...
How can I modify this so that the animations run at random intervals?
Without seeing the rest of your code, I can't say why it's running in an infinite loop. However, you're not passing [actions] as a dependency to your effect (in fact, you're not passing anything as a dependency) - which means that effect will run every time the component renders.
To get the result you're chasing though, I'd probably create a custom hook that takes care of the "re-run after a random delay" logic for you; something like this:
const useRandomlyRepeatedEffect = (effect, deps) => {
// Keep track of the currently running timeout so we can clear it
// if the component unmounts
const timeoutRef = useRef();
useEffect(() => {
const runAndWait = () => {
effect();
const delaySecs = Math.floor(Math.random() * 10);
timeoutRef.current = setTimeout(runAndWait, delaySecs * 1_000);
};
runAndWait();
// Cancel the timeout when the effect if the component unmounts.
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = undefined;
}
};
}, deps);
};
You don't have to do this - you could just have that inline in your component, but I'm a big fan of encapsulating custom logic in hooks.
Then, instead of your useEffect, you should be able to substitute it with useRandomlyRepeatedEffect:
export default const Model = (props) => {
// const actions = /* .... */
useRandomlyRepeatedEffect(() => {
actions.someAction.play;
}, [actions]);
};
Note here that [actions] is being supplied as a dependency to the effect.

Execute Function when a State Variable Changes inside of a useEffect() Hook

so I am trying to create a graph visualization front-end using Antv's G6 and React. I have this useState() variable and function as shown below:
const [hideNode, sethideNode] = useState("");
const hideN = () => {
const node = graph.findById(hideNode);
node.hide();
};
The function is in charge of hiding the selected node. However, the problem with running this function as it is, is that it will raise the error TypeError: Cannot read properties of null (reading 'findById') because graph is assigned inside of the useEffect() hook, as shown below:
useEffect(() => {
if (!graph) {
graph = new G6.Graph();
graph.data(data);
graph.render();
hideN();
}
}, []);
It only works as intended if I call the function hideN() inside of the useEffect() hook, otherwise outside of the useEffect() if I console.log(graph) the result would be undefined.
So I wanted to ask, is there a way I could have this function run when the state changes while inside of the useEffect(), or is there a better way to go about this. I'm sorry I am super new to React so still learning the best way to go about doing something. I'd appreciate any help you guys can provide.
Full code:
import G6 from "#antv/g6";
import React, { useEffect, useState, useRef } from "react";
import { data } from "./Data";
import { NodeContextMenu } from "./NodeContextMenu";
const maxWidth = 1300;
const maxHeight = 600;
export default function G1() {
let graph = null;
const ref = useRef(null);
//Hide Node State
const [hideNode, sethideNode] = useState("");
const hideN = () => {
const node = graph.findById(hideNode);
node.hide();
};
useEffect(() => {
if (!graph) {
graph = new G6.Graph(cfg);
graph.data(data);
graph.render();
hideN();
}
}, []);
return (
<div>
<div ref={ref}>
{showNodeContextMenu && (
<NodeContextMenu
x={nodeContextMenuX}
y={nodeContextMenuY}
node={nodeInfo}
setShowNodeContextMenu={setShowNodeContextMenu}
sethideNode={sethideNode}
/>
)}
</div>
</div>
);
}
export { G1 };
Store graph in a React ref so it persists through rerenders. In hideN use an Optional Chaining operator on graphRef.current to call the findById function.
Add hideNode state as a dependency to the useEffect hook and move the hideN call out of the conditional block that is only instantiating a graph value to store in the ref.
const graphRef = useRef(null);
const ref = useRef(null);
//Hide Node State
const [hideNode, sethideNode] = useState("");
const hideN = () => {
const node = graphRef.current?.findById(hideNode);
node.hide();
};
useEffect(() => {
if (!graphRef.current) {
graphRef.current = new G6.Graph(cfg);
graphRef.current.data(data);
graphRef.current.render();
}
hideN();
}, [hideNode]);

What's wrong with my custom hook in React?

Hi Stack Overflow Community!
I am learning react and now I am practicing custom hooks. I get the example data from here:
https://jsonplaceholder.typicode.com/posts
I've got two components.
App.js
import React from "react";
import useComments from "./hooks/useComments"
const App = () => {
const Comments = useComments();
const renderedItems = Comments.map((comment) => {
return <li key={comment.id}>{comment.title}</li>;
});
return (
<div>
<h1>Comments</h1>
<ul>{renderedItems}</ul>
</div>
);
}
export default App;
useComments.js
import {useState, useEffect} from "react";
const useComments = () => {
const [Comments, setComments] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts", {
"mode": "cors",
"credentials": "omit"
}).then(res => res.json()).then(data => setComments(data))
}, []);
return [Comments];
};
export default useComments;
My output looks like this and i don't know why. There are no warnings or errors.
This line makes Comments be an array:
const [Comments, setComments] = useState([]);
...and then you're wrapping it in an additional array:
return [Comments];
But when you use it, you're treating it as a single dimensional array.
const Comments = useComments();
const renderedItems = Comments.map...
So you'll just need to line those two up. If you want two levels of array-ness (perhaps because you plan to add more to your hook, so that it's returning more things than just Comments), then the component will need to remove one of them. This can be done with destructuring, as in:
const [Comments] = useComments();
Alternatively, if you don't need that complexity, you can change your hook to not add the extra array, and return this:
return Comments;

Is it possible to avoid 'eslint(react-hooks/exhaustive-deps)' error on custom React Hook with useCallback?

Take the following custom React Hook to interact with IntersectionObserver:
import { useCallback, useRef, useState } from 'react';
type IntersectionObserverResult = [(node: Element | null) => void, IntersectionObserverEntry?];
function useIntersectionObserver(options: IntersectionObserverInit): IntersectionObserverResult {
const intersectionObserver = useRef<IntersectionObserver>();
const [entry, setEntry] = useState<IntersectionObserverEntry>();
const ref = useCallback(
(node) => {
if (intersectionObserver.current) {
console.log('[useInterSectionObserver] disconnect(🔴)');
intersectionObserver.current.disconnect();
}
if (node) {
intersectionObserver.current = new IntersectionObserver((entries) => {
console.log('[useInterSectionObserver] callback(🤙)');
console.log(entries[0]);
setEntry(entries[0]);
}, options);
console.log('[useInterSectionObserver] observe(🟢)');
intersectionObserver.current.observe(node);
}
},
[options.root, options.rootMargin, options.threshold]
);
return [ref, entry];
}
export { useIntersectionObserver };
ESLint is complaining about:
React Hook useCallback has a missing dependency: 'options'. Either include it or remove the dependency array.
If I replace the dependencies array with [options], ESLint no longer complains but there's now a much bigger problem, a rendering infinite loop.
What would be the right way to implement this custom React Hook without having the eslint(react-hooks/exhaustive-deps) error showing up?
The fix to this is to destructure the properties you need from options and set them in the dependancy array. That way you don't need options and the hook only gets called when those three values change.
import { useCallback, useRef, useState } from 'react';
type IntersectionObserverResult = [(node: Element | null) => void, IntersectionObserverEntry?];
function useIntersectionObserver(options: IntersectionObserverInit): IntersectionObserverResult {
const intersectionObserver = useRef<IntersectionObserver>();
const [entry, setEntry] = useState<IntersectionObserverEntry>();
const { root, rootMargin, threshold } = options;
const ref = useCallback(
(node) => {
if (intersectionObserver.current) {
console.log('[useInterSectionObserver] disconnect(🔴)');
intersectionObserver.current.disconnect();
}
if (node) {
intersectionObserver.current = new IntersectionObserver((entries) => {
console.log('[useInterSectionObserver] callback(🤙)');
console.log(entries[0]);
setEntry(entries[0]);
}, options);
console.log('[useInterSectionObserver] observe(🟢)');
intersectionObserver.current.observe(node);
}
},
[root, rootMargin, threshold]
);
return [ref, entry];
}
export { useIntersectionObserver };
You should always provide all the necessary values in the dep array to prevent it from using the previous cached function with stale values. One option to fix your situation is to memo the options object so only a new one is being passed when it's values change instead of on every re-render:
// in parent
// this passes a new obj on every re-render
const [ref, entry] = useIntersectionObserver({ root, rootMargin, threshold });
// this will only pass a new obj if the deps change
const options = useMemo(() => ({ root, rootMargin, threshold }), [root, rootMargin, threshold]);
const [ref, entry] = useIntersectionObserver(options);

Resources