I am trying to calculate the width of child component .is it possible to calculate width of children's ? here is my code
here is my code
https://codesandbox.io/s/reverent-hermann-yt7es?file=/src/App.js
<Tabs>
{data.map((i) => (
<li>{i}</li>
))}
</Tabs>
TABS.js
import React, { useEffect, useRef } from "react";
const Tabs = ({ children }) => {
const tabsRef = useRef(null);
const setTabsDimensions = () => {
if (!tabsRef.current) {
return;
}
// initial wrapper width calculation
const blockWidth = tabsRef.current.offsetWidth;
// const showMoreWidth = moreItemRef.current.offsetWidth;
// calculate width and offset for each tab
let tabsTotalWidth = 0;
const tabDimensions = {};
children.forEach((tab, index) => {
if (tab) {
console.log(tab);
// const width = !isMobile ? 200 : 110;
// tabDimensions[index] = {
// width,
// offset: tabsTotalWidth
// };
// tabsTotalWidth += width;
}
});
};
useEffect(() => {
setTabsDimensions();
});
return (
<ul ref={tabsRef} className="rc64nav">
{children}
</ul>
);
};
export default Tabs;
I know using ref we can access the dom property but how to apply ref in children element element.
here i am trying to calculate all item width
children.forEach((tab, index) => {
if (tab) {
console.log(tab);
Just answer your question how to get child's ref.
const Parent = () => {
const childRef = useRef()
// An example of use the childRef
const onClick = () => {
if (!childRef.current) return
console.log(childRef.current.offsetWidth)
}
return <Child childRef={childRef} />
}
const Child = ({ childRef }) => {
return <div ref={childRef}>Hello Child</div>
}
NOTE: the way I pass ref here is exactly the same way you pass parent method to child. And also ref supports two formats, i'm using the simple one, but you can also use () => {} format. https://reactjs.org/docs/refs-and-the-dom.html
Related
I want to run a function on the parent component in the child component.
Eventually, I want to have the function run when the scroll goes to that position.
UseImperativeHandle was used, but props did not apply. Is there a way to apply props in the useImperativeHandle?
Also, is it correct to use IntersectionObserver this way?
child Components
function Percent(props, ref) {
useImperativeHandle(ref,() => ({
percentst: () => {
var cnt = document.querySelectorAll(".count")[props.num];
var water = document.querySelectorAll(".water")[props.num];
var percent = cnt.innerText;
var interval;
interval = setInterval(function () {
percent++;
cnt.innerHTML = percent;
water.style.transform = 'translate(0' + ',' + (100 - percent) + '%)';
if (percent == props.percent) {
clearInterval(interval);
}
}, 80);
}
}));
}
export default forwardRef(Percent);
parent component
function About(props) {
const containerRef = useRef();
const myRef = useRef();
const [isVisible, setIsVisible] = useState(false);
const callbackFunction = (entries) => {
const [entry] = entries;
setIsVisible(entry.isIntersecting);
};
const options = {
root: document.getElementById('skills'),
rootMargin: '0px',
threshold: 1
};
useEffect(() => {
const observer = new IntersectionObserver(callbackFunction, options);
console.log(containerRef.current)
if (containerRef.current) observer.observe(containerRef.current);
return () => {
myRef.current.percentst()
if (containerRef.current) observer.unobserve(containerRef.current);
};
}, [containerRef, options]);
return(
<div ref={containerRef}></div>
<Percent ref={myRef} />
)
}
export default About;
Two ways of doing this
Pass your method as a prop to the child (this may not be desirable)
Use custom event listeners/triggers
There are some packages out there that will help with events such as https://www.npmjs.com/package/react-custom-events
My goal is to make it so I know which video the user has seen in the viewport latest. This was working until I turned the videos into functional React components, which I can't figure out how to check the ref until after the inital render of the React parent. This is currently the top part of the component:
function App() {
const ref1 = useRef(null);
const ref2 = useRef(null);
const ref3 = useRef(null);
function useIsInViewport(ref) {
const [isIntersecting, setIsIntersecting] = useState(false);
const observer = useMemo(
() =>
new IntersectionObserver(([entry]) =>
setIsIntersecting(entry.isIntersecting)
),
[]
);
useEffect(() => {
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [ref, observer]);
return isIntersecting;
}
var videoProxy = new Proxy(videoViewports, {
set: function (target, key, value) {
// console.log("value " + value)
// console.log("key " + key)
console.log(videoViewports);
if (value) {
setMostRecentVideo(key);
//console.log("Most Rec: " + mostRecentVideo);
}
target[key] = value;
return true;
},
});
const [isGlobalMute, setIsGlobalMute] = useState(true);
const [mostRecentVideo, setMostRecentVideo] = useState("");
videoProxy["Podcast 1"] = useIsInViewport(ref1);
videoProxy["Podcast 2"] = useIsInViewport(ref2);
videoProxy["Podcast 3"] = useIsInViewport(ref3);
And each component looks like this:
<VideoContainer
ref={ref1}
videoProxy={videoProxy}
mostRecentVideo={mostRecentVideo}
setMostRecentVideo={setMostRecentVideo}
title="Podcast 1"
isGlobalMute={isGlobalMute}
setIsGlobalMute={setIsGlobalMute}
videoSource={video1}
podcastName={podcastName}
networkName={networkName}
episodeName={episodeName}
episodeDescription={episodeDescription}
logo={takeLogo}
muteIcon={muteIcon}
unmuteIcon={unmuteIcon}
></VideoContainer>
I had moved the logic for checking if the component was in the viewport into each component, but then it was impossible to check which component was the LATEST to move into viewport. I tried looking online and I don't understand how I would forward a ref here, or how to get the useIsInViewport to only start working after the initial render since it can't be wrapped in a useEffect(() => {}, []) hook. Maybe I'm doing this completely the wrong way with the wrong React Hooks, but I've been bashing my head against this for so long...
First of all: I'm not quite sure, if a Proxy.set is the right way of accomplishing your goal (depends on your overall app architecture). Because setting data does not always mean, the user has really seen the video or is in the viewport.
I've created a simple solution that uses two components. First the a VideoList that contains all videos and manages the viewport calculations so you don't have thousands of event listeners on resize, scroll and so on (or Observers respectively).
The Video component is a forwardRef component, so we get the ref of the rendered HTML video element (or in the case of this example, the encompassing div).
import { forwardRef, useCallback, useEffect, useState, createRef } from "react";
function inViewport(el) {
if (!el) {
return false;
}
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
const Video = forwardRef((props, ref) => {
return (
<div ref={ref}>
<p>{props.source}</p>
<video {...props} />
</div>
);
});
const VideoList = ({ sources }) => {
const sourcesLength = sources.length;
const [refs, setRefs] = useState([]);
useEffect(() => {
// set refs
setRefs((r) =>
Array(sources.length)
.fill()
.map((_, i) => refs[i] || createRef())
);
}, [sourcesLength]);
const isInViewport = useCallback(() => {
// this returns only the first but you can also apply a `filter` instead of the index
const videoIndex = refs.findIndex((ref) => {
return inViewport(ref.current);
});
if (videoIndex < 0) {
return;
}
console.log(`lastSeen video is ${sources[videoIndex]}`);
}, [refs, sources]);
useEffect(() => {
// add more listeners like resize, or use observer
document.addEventListener("scroll", isInViewport);
document.addEventListener("resize", isInViewport);
return () => {
document.removeEventListener("scroll", isInViewport);
document.removeEventListener("resize", isInViewport);
};
}, [isInViewport]);
return (
<div>
{sources.map((source, i) => {
return <Video ref={refs[i]} source={source} key={i} />;
})}
</div>
);
};
export default function App() {
const sources = ["/url/to/video1.mp4", "/url/to/video1.mp4"];
return (
<div className="App">
<VideoList sources={sources} />
</div>
);
}
Working example that should lead you into the right directions: https://codesandbox.io/s/distracted-waterfall-go6g7w?file=/src/App.js:0-1918
Please go over to https://stackoverflow.com/a/54633947/1893976 to see, why I'm using a useState for the ref list.
I'm making a "sticky" header when scrollY meets offsetTop, but I don't want to hard code the offset. I've been attempting to store the initial offsetTop which can then be checked inside the isSticky() method attached to the scroll listener.
So far I've been unable to make the value stick (no pun intended) and it appears to be null inside the method being run.
const ProfileHeader: React.FC<ProfileHeaderData> = ({ profile }) => {
const bannerUrl = getBannerImageUrl(profile.id, null, profile.bannerImageVersion);
const logoUrl = getLogoImageUrl(profile.id, null, profile.logoImageVersion);
const headerRef = useRef<HTMLDivElement>(null);
const [sticky, setSticky] = useState("");
const [headerOffSet, setHeaderOffSet] = useState<number>(null);
console.log(`Outside: ${headerOffSet}`);
// on render, set listener
useEffect(() => {
console.log(`Render: ${headerRef.current.offsetTop}`);
setHeaderOffSet(headerRef.current.offsetTop);
window.addEventListener("scroll", isSticky);
return () => {
window.removeEventListener("scroll", isSticky);
};
}, []);
const isSticky = () => {
/* Method that will fix header after a specific scrollable */
const scrollTop = window.scrollY;
let originalOffset = headerOffSet ?? headerRef.current.offsetTop;
if (headerOffSet === null) {
console.log(`Setting header off set`);
setHeaderOffSet(originalOffset);
}
console.log(`top: ${scrollTop} | offset: ${originalOffset} | state: ${headerOffSet}`);
const stickyClass = scrollTop >= originalOffset ? styles.sticky : "";
setSticky(stickyClass);
};
return (
<div className={styles.container}>
<div className={styles.bannerContainer}>
{profile.bannerImageVersion && (
<Image
src={bannerUrl}
layout='fill'
className={styles.banner}
/>
)}
</div>
<div ref={headerRef} className={`${styles.header} ${sticky}`}>
<div className={styles.logoContainer}>
{profile.logoImageVersion && (
<Image
src={logoUrl}
layout='fill'
className={styles.logo}
/>
)}
</div>
<FlagCircleIcon {...profile.country} size={32} />
<h1 className={styles.displayName}>{profile.displayName}</h1>
</div>
</div>
)
}
On the initial page load, I get the following console output:
As I start to scroll, the output is as follows:
Seems like the state is never set?
Found a blog post explaining that you need to use a reference (useRef) and access the value via that inside the listener.
https://medium.com/geographit/accessing-react-state-in-event-listeners-with-usestate-and-useref-hooks-8cceee73c559
This references another SO post:
React useState hook event handler using initial state
EDIT
Providing working example based on original question code and solution provided by the blog post. The change the key change is to have a references to the useState variables and read from those inside the event listener.
import { useEffect, useRef, useState } from 'react';
import styles from './testComponent.module.scss';
export const TestComponent: React.FC = ({ children }) => {
const headerRef = useRef<HTMLDivElement>(null);
const [sticky, _setSticky] = useState(false);
const stickyRef = useRef(sticky);
const setSticky = (data: boolean) => {
stickyRef.current = data;
_setSticky(data);
}
const [headerOffSet, _setHeaderOffSet] = useState<number>(null);
const headerOffSetRef = useRef(headerOffSet);
const setHeaderOffSet = (data: number) => {
headerOffSetRef.current = data;
_setHeaderOffSet(data);
}
const isSticky = () => {
const scrollTop = window.scrollY;
const isSticky = scrollTop >= headerOffSetRef.current;
setSticky(isSticky);
};
// on render, set listener
useEffect(() => {
setHeaderOffSet(headerRef.current.offsetTop);
window.addEventListener("scroll", isSticky);
return () => {
window.removeEventListener("scroll", isSticky);
};
}, []);
return (
<div className={styles.container}>
<div ref={headerRef} className={`${styles.header} ${sticky ? styles.isSticky : null}`}>
{children}
</div>
</div>
)
}
I need to add draggable divs, one for every image in an array of images, to a container.
The below works BUT only on the second drag. i.e. I have to drag the component twice for it to move. It obviously has something to do with the hooks used and how the divs are being created. Any help will be appreciated.
Code sandbox
const App = ({images}) => {
const [selectedImages, setSelectedImages] = useState(images)
const [dragId, setDragId] = useState()
const handleDrag = (ev) => {
setDragId(ev.currentTarget.id)
}
const handleDrop = (ev) => {
const sortedImages = sortImages(selectedImages, dragId)
setSelectedImages(sortedImages)
}
return (
<Container
images={selectedImages}
handleDrag={handleDrag}
handleDrop={handleDrop}
/>
)
}
export default App
const Container = ({ images, handleDrag, handleDrop }) => {
const ref = useRef(null)
useEffect(() => {
if (containerRef.current) {
for (let i = 0; i < images.length; ++i) {
const draggable = document.createElement('div')
draggable.ondragstart = handleDrag
draggable.ondrop = handleDrop
const style = 'position: absolute; ...'
draggable.setAttribute('style', style)
draggable.setAttribute('draggable', true)
draggable.setAttribute('id', images[i].id)
ref.current.appendChild(draggable)
}
}
}, [images, ref, handleDrag, handleDrop])
return (
<div className={'relative'}>
<div ref={ref} />
</div>
)
}
export default Container
It looks like it is setting the dragId on first drag and then only on the second drag it actually drags and drops the div. How can I set this up so that it drags and drops on the first try?
I should have just used React to add the functions
const Container = ({ images, handleDrag, handleDrop }) => {
const containerRef = useRef(null)
return (
<div ref={containerRef}>
{images.map((image) => (
<div
key={image.id}
id={image.id}
draggable
onDragOver={(ev) => ev.preventDefault()}
onDrop={handleDrop}
onDragStart={handleDrag}
style={{
position: 'absolute'...`,
}}
/>
))}
</div>
)
}
export default Container
Sandbox
I'm dynamically rendering a list of Symbol(react.element) by mapping into an array and placing each of its elements HTML tags. My question is therefore: how can I get the height of each of the rendered Symbol(react.element)? This seems not to be in the Symbol(react.element)'s object.
Thanks in advance for your help
Actually, if you are using Functional Components, would be better to isolate this resize logic in a custom hook instead of leave it inside the component. You can create a custom hook like this:
const useResize = (myRef) => {
const [width, setWidth] = useState(0)
const [height, setHeight] = useState(0)
const handleResize = () => {
setWidth(myRef.current.offsetWidth)
setHeight(myRef.current.offsetHeight)
}
useEffect(() => {
myRef.current && myRef.current.addEventListener('resize', handleResize)
return () => {
myRef.current.removeEventListener('resize', handleResize)
}
}, [myRef])
return { width, height }
}
and then you can use it like:
const MyComponent = () => {
const componentRef = useRef()
const { width, height } = useResize(componentRef)
return (
<div ref={componentRef}>
<p>width: {width}px</p>
<p>height: {height}px</p>
<div/>
)
}
class MyComponent extends Component {
constructor(props){
super(props)
this.myDiv = React.createRef()
}
componentDidMount () {
console.log(this.myDiv.current.offsetHeight)
}
render () {
return (
<div ref={this.myDiv}>element</div>
)
}
}
A modified version of Marcos answer.
I've placed a rendering bool to make sure all data is rendered before placing the height and width. This is to be sure that the height is calculated with all required elements in place instead of risking receiving an incorrect height and width.
useResize hook placed in a separate folder:
import { useState, useEffect, useCallback } from 'react';
export const useResize = (myRef: React.MutableRefObject<any>, rendering: boolean) => {
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
const handleResize = useCallback(() => {
setWidth(myRef.current.offsetWidth);
setHeight(myRef.current.offsetHeight);
}, [myRef]);
useEffect(() => {
if (!rendering) {
myRef.current && myRef.current.addEventListener('resize',
handleResize(), { once: true });
}
}, [myRef, handleResize, rendering]);
return { width, height };
Example of usage:
const MyComponent = ({ A, B }) => {
// A and B is data that is required in component
const componentRef = useRef()
const { width, height } = useResize(componentRef, !A || !B)
if (!A || !B) return;
return (
<div ref={componentRef}>
<p>{A} {width}px</p>
<p>{B} {height}px</p>
<div/>
)
}
const componentRef = useRef(null)
and div ref={componentRef}