I am trying to create a code that checks whether the div element is outside the window, or not. The issue with the code is that it ignores the result on the isOpen state update, so as ignores if the element is slightly outside the window screen. Is there a chance it can be further adjusted?
export const useOnScreen = (ref, rootMargin = '0px') => {
const [isIntersecting, setIntersecting] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
setIntersecting(entry.isIntersecting);
},
{
rootMargin
}
);
if (ref.current) {
observer.observe(ref.current);
}
return () => {
observer.unobserve(ref.current);
};
}, []);
return isIntersecting;
};
export default useOnScreen;
Usage:
const packRef = useRef(null);
const isVisible = useOnScreen(packRef);
{ !optionsOpen && (
<div className="" ref={ packRef }>
Assigning the class with Tailwind
<div
className={ classNames('absolute top-full right-0 w-full bg-white mt-2 rounded-lg overflow-hidden shadow-2xl', {
'!bottom-full': !isVisible
}) }
ref={ observe }
>
Since the element you're trying to observe doesn't exist when the useEffect is called, the ref is null, and nothing is observed. When the element appears it's not magically observed.
Instead of an object ref, use a callback ref. The callback ref would be called as soon as the element appears. It would initialize the observer if needed, and would observe the element.
You don't need to unobserve a removed item in this case, since the observer maintains weak references to the observed elements.
Demo - scroll up and down and toggle the element:
const { useRef, useState, useEffect, useCallback } = React;
const useOnScreen = (rootMargin = '0px') => {
const observer = useRef();
const [isIntersecting, setIntersecting] = useState(false);
// disconnect observer on unmount
useEffect(() => () => {
if(observer) observer.disconnect(); // or observer?.disconnect() if ?. is supported
}, []);
const observe = useCallback(element => {
// init observer if one doesn't exist
if(!observer.current) observer.current = new IntersectionObserver(
([entry]) => { setIntersecting(entry.isIntersecting); },
{ rootMargin }
);
// observe an element
if(element) observer.current.observe(element)
}, [rootMargin]);
return [isIntersecting, observe];
};
const Demo = () => {
const [isOpen, setIsOpen] = useState(false);
const [isVisible, observe] = useOnScreen();
return (
<div className="container">
<div className="header">
<button onClick={() => setIsOpen(!isOpen)}>Toggle {isOpen ? 'on' : 'off'}</button>
<div>Visible {isVisible ? 'yes' : 'no'}</div>
</div>
{isOpen && <div className="child" ref={observe} />}
</div>
);
};
ReactDOM
.createRoot(root)
.render(<Demo />);
.container {
height: 300vh;
}
.header {
top: 10px;
position: sticky;
}
.child {
height: 30vh;
margin-top: 110vh;
background: blue;
}
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
If you want the observer to trigger only when 100% of the element is visible set the threshold to 1 (or any percent the fits your case):
const { useRef, useState, useEffect, useCallback } = React;
const useOnScreen = (rootMargin = '0px') => {
const observer = useRef();
const [isIntersecting, setIntersecting] = useState(false);
// disconnect observer on unmount
useEffect(() => () => {
if(observer) observer.disconnect(); // or observer?.disconnect() if ?. is supported
}, []);
const observe = useCallback(element => {
// init observer if one doesn't exist
if(!observer.current) observer.current = new IntersectionObserver(
([entry]) => { setIntersecting(entry.isIntersecting); },
{
rootMargin,
threshold: 1 // define the threshold to control what amount of visibility triggers the observer
}
);
// observe an element
if(element) observer.current.observe(element)
}, [rootMargin]);
return [isIntersecting, observe];
};
const Demo = () => {
const [isOpen, setIsOpen] = useState(false);
const [isVisible, observe] = useOnScreen();
return (
<div className="container">
<div className="header">
<button onClick={() => setIsOpen(!isOpen)}>Toggle {isOpen ? 'on' : 'off'}</button>
<div>Fully visible {isVisible ? 'yes' : 'no'}</div>
</div>
{isOpen && (
<div
className={classNames({ child: true, visible: isVisible })}
ref={observe} />
)}
</div>
);
};
ReactDOM
.createRoot(root)
.render(<Demo />);
.container {
height: 300vh;
}
.header {
top: 10px;
position: sticky;
}
.child {
height: 30vh;
margin-top: 110vh;
background: blue;
}
.visible {
border: 2px solid red;
}
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.3.2/index.min.js" integrity="sha512-GqhSAi+WYQlHmNWiE4TQsVa7HVKctQMdgUMA+1RogjxOPdv9Kj59/no5BEvJgpvuMTYw2JRQu/szumfVXdowag==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div id="root"></div>
Related
I have two components, Container and Item.
Container contains Item.
Item contains a button and a div.
These components have the following behaviors:
Container: When I click outside of Container it should disappear, I'm achieving this by using a custom hook that detects clicks outside of components. This works just fine.
Item: When I click in the div which is inside Item it should disappear, I'm achieving this by setting a boolean state. This also works but the problem here is that Container also disappears.
Container
const Container = ({ setDisplay }) => {
const containerRef = useRef(null);
useClickOutside(containerRef, () => {
//code to make Container disappear that is not relevant for the issue
setDisplay(false)
});
return (
<div ref={containerRef} className='container'>
<Item />
</div>
);
};
Item
const Item = () => {
const [displayItem, setDisplayItem] = useState(false);
return (
<div>
<button onClick={() => setDisplayItem(true)}>Show Item's content</button>
{displayItem && (
<div
className='item-content'
onClick={() => setDisplayItem(false)}
/>
)}
</div>
);
};
useClickOutside
const useClickOutside = (ref, handler) => {
useEffect(() => {
const trigger = e => {
if (!(ref?.current?.contains(e.target))) handler();
}
document.addEventListener('click', trigger);
return () => document.removeEventListener('click', trigger);
}, [handler, ref])
}
Why is this happening and how can I prevent it?
Note: I have to use that hook.
Both the listeners are being attached to the bubbling phase, so the inner ones trigger first.
When the item is shown, and when it's clicked, this runs:
<div
className='item-content'
onClick={() => setDisplayItem(false)}
>item content</div>
As a result, before the event propagates outward, setDisplayItem(false) causes this .item-content element to be removed from the DOM. See here, how the parent no longer exists afterwards:
const Container = ({ setDisplay }) => {
const containerRef = React.useRef(null);
useClickOutside(containerRef, () => {
//code to make Container disappear that is not relevant for the issue
console.log('making container disappear');
});
return (
<div ref={containerRef} className='container'>
container
<Item />
</div>
);
};
const Item = () => {
const [displayItem, setDisplayItem] = React.useState(false);
return (
<div>
<button onClick={() => setDisplayItem(true)}>Show Item's content</button>
{displayItem && (
<div
className='item-content'
onClick={() => setDisplayItem(false)}
>item content</div>
)}
</div>
);
};
const useClickOutside = (ref, handler) => {
React.useEffect(() => {
const trigger = e => {
console.log(e.target.parentElement);
if (!(ref.current.contains(e.target))) handler();
}
document.addEventListener('click', trigger);
return () => document.removeEventListener('click', trigger);
}, [handler, ref])
}
ReactDOM.render(<Container />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='react'></div>
You can fix this by changing useClickOutside to also check whether the node is connected or not. If it's not connected, the element is no longer in the DOM due to a state change and rerender - so the click that was made wasn't definitely outside the ref.current, so the handler shouldn't run.
const trigger = e => {
const { current } = ref;
if (e.target.isConnected && !current.contains(e.target)) {
const Container = ({ setDisplay }) => {
const containerRef = React.useRef(null);
useClickOutside(containerRef, () => {
//code to make Container disappear that is not relevant for the issue
console.log('making container disappear');
});
return (
<div ref={containerRef} className='container'>
container
<Item />
</div>
);
};
const Item = () => {
const [displayItem, setDisplayItem] = React.useState(false);
return (
<div>
<button onClick={() => setDisplayItem(true)}>Show Item's content</button>
{displayItem && (
<div
className='item-content'
onClick={() => setDisplayItem(false)}
>item content</div>
)}
</div>
);
};
const useClickOutside = (ref, handler) => {
React.useEffect(() => {
const trigger = e => {
const { current } = ref;
if (e.target.isConnected && !current.contains(e.target)) {
console.log(current.parentElement);
handler();
}
}
document.addEventListener('click', trigger);
return () => document.removeEventListener('click', trigger);
}, [handler, ref])
}
ReactDOM.render(<Container />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='react'></div>
Hello I'm creating a progerss bar using ReactJS but I have a problem
My progress bar is going to be a React component and I will pass the progress value in the props
For some reason it seems like document.getElementById() does not find anything
import './MyProgressBar.css'
const MyProgressBar = (props) => {
const value = props.value
const progressbar = document.getElementById("hello");
progressbar.style.width = value + "%"
return (
<div class="progress">
<div id="hello" class="color"></div>
</div>
)
}
export default MyProgressBar
It throws an error that says "Cannot read property 'style' of null at MyProgressBar (MyProgressBar.js:9)....."
The reason for the error is that the element doesn't exist yet as of when you go looking for it. You could "fix" that with a useEffect or useLayoutEffect callback, but that wouldn't be the React approach. Your component will be called to re-render when the props change, so handle rendering in the new state directly:
const MyProgressBar = ({value}) => {
return (
<div class="progress">
<div class="color" style={{width: value + "%"}}></div>
</div>
);
};
This also has the advantage that you can have multiple MyProgressBar instances in the page at the same time.
Live Example:
const {useState, useEffect} = React;
const MyProgressBar = ({value}) => {
return (
<div className="progress">
<div className="color" style={{width: value + "%"}}></div>
</div>
);
};
const App = () => {
const [bar1, setBar1] = useState(0);
const [bar2, setBar2] = useState(0);
useEffect(() => {
const t1 = setInterval(() => {
setBar1(b1 => {
if (b1 < 100) {
++b1;
return b1;
}
clearInterval(t1);
return b1;
});
}, 200);
const t2 = setInterval(() => {
setBar2(b2 => {
if (b2 < 100) {
++b2;
return b2;
}
clearInterval(t2);
return b2;
});
}, 400);
}, []);
return <div>
<div>
Every 200ms:
<MyProgressBar value={bar1} />
</div>
<div>
Every 400ms:
<MyProgressBar value={bar2} />
</div>
</div>;
};
ReactDOM.render(<App/>, document.getElementById("root"));
.color {
background-color: blue;
height: 100%;
}
.progress {
height: 1em;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
This code is running before there is anything on the DOM.
You can use it inside a useEffect, running after the render.
useEffect(() => {
const value = props.value
const progressbar = document.getElementById("hello");
progressbar.style.width = value + "%";
}, [props.value]);
You can also use useRef
You document.getElementById is run before actual rendering, so it could not find any element with that Id
It's not reccomend to use document.getElementById in your Reactjs code, use useRef instead:
function App() {
const divRef = React.useRef();
React.useEffect(() => {
console.log(divRef.current);
}, []);
return (
<div class="progress">
<div ref={divRef} id="hello" class="color"></div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
As explained earlier document.getElementById won't work in case of react. Because it is running before actual DOM is being created.
First method is to use useEffect
import './MyProgressBar.css'
import { useEffect, useState } from 'react';
const MyProgressBar = (props) => {
const [progressbarWidth, setProgressbarWidth] = useState(0)
useEffect(() => {
const value = props.value
setProgressbarWidth(value)
}, [props.value])
return (
<div class="progress" style={{ width: `${progressbarWidth}%` }}>
<div id="hello" class="color"></div>
</div>
)
}
export default MyProgressBar
Second method is to use useRef
import './MyProgressBar.css'
import { useEffect, useRef } from 'react';
const MyProgressBar = (props) => {
const progressbarRef = useRef()
useEffect(() => {
const value = props.value
progressbarRef.current.style.width = value + "%"
}, [props.value])
return (
<div class="progress" ref={progressbarRef}>
<div id="hello" class="color"></div>
</div>
)
}
export default MyProgressBar
In react you don't have to access elements by id.
I encourage you to do something like this:
import './MyProgressBar.css'
const MyProgressBar = (props) => {
const value = props.value
return (
<div class="progress">
<div style={{width: `${value}%` }}></div>
</div>
)
}
export default MyProgressBar
My website is too heavy because it downloads 200-400 images after fetching data from the server (Google's Firebase Firestore).
I came up with two solutions and I hope somebody answers one of them:
I want to set each img to have a loading state and enable visitors to see the placeholder image until it is loaded. As I don't know how many images I get until fetching data from the server, I find it hard to initialize image loading statuses by useState. Is this possible? Then, how?
How can I Lazy load images? Images are initialized with a placeholder. When a scroll comes near an image, the image starts to download replacing the placeholder.
function sample() {}{
const [items, setItems] = useState([])
const [imgLoading, setImgLoading] = useState(true) // imgLoading might have to be boolean[]
useEffect(() => {
axios.get(url).
.then(response => setItems(response.data))
}, [])
return (
items.map(item => <img src={item.imageUrl} onLoad={setImgLoading(false)} />)
)
}
I would create an Image component that would handle it's own relevant states. Then inside this component, I would use IntersectionObserver API to tell if the image's container is visible on user's browser or not.
I would have isLoading and isInview states, isLoading will be always true until isInview updates to true.
And while isLoading is true, I would use null as src for the image and will display the placeholder.
Load only the src when container is visible on user's browser.
function Image({ src }) {
const [isLoading, setIsLoading] = useState(true);
const [isInView, setIsInView] = useState(false);
const root = useRef(); // the container
useEffect(() => {
// sets `isInView` to true until root is visible on users browser
const observer = new IntersectionObserver(onIntersection, { threshold: 0 });
observer.observe(root.current);
function onIntersection(entries) {
const { isIntersecting } = entries[0];
if (isIntersecting) { // is in view
observer.disconnect();
}
setIsInView(isIntersecting);
}
}, []);
function onLoad() {
setIsLoading((prev) => !prev);
}
return (
<div
ref={root}
className={`imgWrapper` + (isLoading ? " imgWrapper--isLoading" : "")}
>
<div className="imgLoader" />
<img className="img" src={isInView ? src : null} alt="" onLoad={onLoad} />
</div>
);
}
I would also have CSS styles that will toggle the placeholder and image's display property.
.App {
--image-height: 150px;
--image-width: var(--image-height);
}
.imgWrapper {
margin-bottom: 10px;
}
.img {
height: var(--image-height);
width: var(--image-width);
}
.imgLoader {
height: 150px;
width: 150px;
background-color: red;
}
/* container is loading, hide the img */
.imgWrapper--isLoading .img {
display: none;
}
/* container not loading, display img */
.imgWrapper:not(.imgWrapper--isLoading) .img {
display: block;
}
/* container not loading, hide placeholder */
.imgWrapper:not(.imgWrapper--isLoading) .imgLoader {
display: none;
}
Now my Parent component, will do the requests for all the image urls. It would also have its own isLoading state that when set true would display its own placeholder. When the image url's request resolves, I would then map on each url to render my Image components.
export default function App() {
const [imageUrls, setImageUrls] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchImages().then((response) => {
setImageUrls(response);
setIsLoading((prev) => !prev);
});
}, []);
const images = imageUrls.map((url, index) => <Image key={index} src={url} />);
return <div className="App">{isLoading ? "Please wait..." : images}</div>;
}
There are libraries for this, but if you want to roll your own, you can use an IntersectionObserver, something like this:
const { useState, useRef, useEffect } = React;
const LazyImage = (imageProps) => {
const [shouldLoad, setShouldLoad] = useState(false);
const placeholderRef = useRef(null);
useEffect(() => {
if (!shouldLoad && placeholderRef.current) {
const observer = new IntersectionObserver(([{ intersectionRatio }]) => {
if (intersectionRatio > 0) {
setShouldLoad(true);
}
});
observer.observe(placeholderRef.current);
return () => observer.disconnect();
}
}, [shouldLoad, placeholderRef]);
return (shouldLoad
? <img {...imageProps}/>
: <div className="img-placeholder" ref={placeholderRef}/>
);
};
ReactDOM.render(
<div className="scroll-list">
<LazyImage src='https://i.insider.com/536a52d9ecad042e1fb1a778?width=1100&format=jpeg&auto=webp'/>
<LazyImage src='https://www.denofgeek.com/wp-content/uploads/2019/12/power-rangers-beast-morphers-season-2-scaled.jpg?fit=2560%2C1440'/>
<LazyImage src='https://i1.wp.com/www.theilluminerdi.com/wp-content/uploads/2020/02/mighty-morphin-power-rangers-reunion.jpg?resize=1200%2C640&ssl=1'/>
<LazyImage src='https://m.media-amazon.com/images/M/MV5BNTFiODY1NDItODc1Zi00MjE2LTk0MzQtNjExY2I1NTU3MzdiXkEyXkFqcGdeQXVyNzU1NzE3NTg#._V1_CR0,45,480,270_AL_UX477_CR0,0,477,268_AL_.jpg'/>
</div>,
document.getElementById('app')
);
.scroll-list > * {
margin-top: 400px;
}
.img-placeholder {
content: 'Placeholder!';
width: 400px;
height: 300px;
border: 1px solid black;
background-color: silver;
}
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
This code is having them load as soon as the placeholder is visible on the screen, but if you want a larger detection margin, you can tweak the rootMargin option of the IntersectionObserver so it starts loading while still slightly off screen.
Map the response data to an array of "isLoading" booleans, and update the callback to take the index and update the specific "isLoading" boolean.
function Sample() {
const [items, setItems] = useState([]);
const [imgLoading, setImgLoading] = useState([]);
useEffect(() => {
axios.get(url).then((response) => {
const { data } = response;
setItems(data);
setImgLoading(data.map(() => true));
});
}, []);
return items.map((item, index) => (
<img
src={item.imageUrl}
onLoad={() =>
setImgLoading((loading) =>
loading.map((el, i) => (i === index ? false : el))
)
}
/>
));
}
I am trying to find out if a div has overflown text and show show more link if it does. I found this stackoverflow answer to check if a div is overflowing. According to this answer, I need to implement a function which can access styles of the element in question and do some checks to see if it is overflowing. How can I access the styles of an element. I tried 2 ways
1. Using ref
import React from "react";
import "./styles.css";
export default function App(props) {
const [showMore, setShowMore] = React.useState(false);
const onClick = () => {
setShowMore(!showMore);
};
const checkOverflow = () => {
const el = ref.current;
const curOverflow = el.style.overflow;
if ( !curOverflow || curOverflow === "visible" )
el.style.overflow = "hidden";
const isOverflowing = el.clientWidth < el.scrollWidth
|| el.clientHeight < el.scrollHeight;
el.style.overflow = curOverflow;
return isOverflowing;
};
const ref = React.createRef();
return (
<>
<div ref={ref} className={showMore ? "container-nowrap" : "container"}>
{props.text}
</div>
{(checkOverflow()) && <span className="link" onClick={onClick}>
{showMore ? "show less" : "show more"}
</span>}
</>
)
}
2. Using forward ref
Child component
export const App = React.forwardRef((props, ref) => {
const [showMore, setShowMore] = React.useState(false);
const onClick = () => {
setShowMore(!showMore);
};
const checkOverflow = () => {
const el = ref.current;
const curOverflow = el.style.overflow;
if (!curOverflow || curOverflow === "visible") el.style.overflow = "hidden";
const isOverflowing =
el.clientWidth < el.scrollWidth || el.clientHeight < el.scrollHeight;
el.style.overflow = curOverflow;
return isOverflowing;
};
return (
<>
<div ref={ref} className={showMore ? "container-nowrap" : "container"}>
{props.text}
</div>
{checkOverflow() && (
<span className="link" onClick={onClick}>
{showMore ? "show less" : "show more"}
</span>
)}
</>
);
});
Parent component
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
const rootElement = document.getElementById("root");
const ref = React.createRef();
ReactDOM.render(
<React.StrictMode>
<App
ref={ref}
text="Start editing to see some magic happen! Click show more to expand and show less to collapse the text"
/>
</React.StrictMode>,
rootElement
);
But I got the following error in both approaches - Cannot read property 'style' of null.
What am I doing wrong? How can I achieve what I want?
As Jamie Dixon suggested in the comment, I used useLayoutEffect hook to set showLink true. Here is the code
Component
import React from "react";
import "./styles.css";
export default function App(props) {
const ref = React.createRef();
const [showMore, setShowMore] = React.useState(false);
const [showLink, setShowLink] = React.useState(false);
React.useLayoutEffect(() => {
if (ref.current.clientWidth < ref.current.scrollWidth) {
setShowLink(true);
}
}, [ref]);
const onClickMore = () => {
setShowMore(!showMore);
};
return (
<div>
<div ref={ref} className={showMore ? "" : "container"}>
{props.text}
</div>
{showLink && (
<span className="link more" onClick={onClickMore}>
{showMore ? "show less" : "show more"}
</span>
)}
</div>
);
}
CSS
.container {
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 200px;
}
.link {
text-decoration: underline;
cursor: pointer;
color: #0d6aa8;
}
We could create a custom hooks to know if we have overflow.
import * as React from 'react';
const useIsOverflow = (ref, isVerticalOverflow, callback) => {
const [isOverflow, setIsOverflow] = React.useState(undefined);
React.useLayoutEffect(() => {
const { current } = ref;
const { clientWidth, scrollWidth, clientHeight, scrollHeight } = current;
const trigger = () => {
const hasOverflow = isVerticalOverflow ? scrollHeight > clientHeight : scrollWidth > clientWidth;
setIsOverflow(hasOverflow);
if (callback) callback(hasOverflow);
};
if (current) {
trigger();
}
}, [callback, ref, isVerticalOverflow]);
return isOverflow;
};
export default useIsOverflow;
and just check in your component
import * as React from 'react';
import { useIsOverflow } from './useIsOverflow';
const App = () => {
const ref = React.useRef();
const isOverflow = useIsOverflow(ref);
console.log(isOverflow);
// true
return (
<div style={{ overflow: 'auto', height: '100px' }} ref={ref}>
<div style={{ height: '200px' }}>Hello React</div>
</div>
);
};
Thanks to Robin Wieruch for his awesome articles
https://www.robinwieruch.de/react-custom-hook-check-if-overflow/
Solution using TS and Hooks
Create your custom hook:
import React from 'react'
interface OverflowY {
ref: React.RefObject<HTMLDivElement>
isOverflowY: boolean
}
export const useOverflowY = (
callback?: (hasOverflow: boolean) => void
): OverflowY => {
const [isOverflowY, setIsOverflowY] = React.useState(false)
const ref = React.useRef<HTMLDivElement>(null)
React.useLayoutEffect(() => {
const { current } = ref
if (current) {
const hasOverflowY = current.scrollHeight > window.innerHeight
// RHS of assignment could be current.scrollHeight > current.clientWidth
setIsOverflowY(hasOverflowY)
callback?.(hasOverflowY)
}
}, [callback, ref])
return { ref, isOverflowY }
}
use your hook:
const { ref, isOverflowY } = useOverflowY()
//...
<Box ref={ref}>
...code
Import your files as need be and update code to your needs.
So I have an array of data in and I am generating a list of components with that data. I'd like to have a ref on each generated element to calculate the height.
I know how to do it with a Class component, but I would like to do it with React Hooks.
Here is an example explaining what I want to do:
import React, {useState, useCallback} from 'react'
const data = [
{
text: 'test1'
},
{
text: 'test2'
}
]
const Component = () => {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<div>
{
data.map((item, index) =>
<div ref={measuredRef} key={index}>
{item.text}
</div>
)
}
</div>
)
}
Not sure i fully understand your intent, but i think you want something like this:
const {
useState,
useRef,
createRef,
useEffect
} = React;
const data = [
{
text: "test1"
},
{
text: "test2"
}
];
const Component = () => {
const [heights, setHeights] = useState([]);
const elementsRef = useRef(data.map(() => createRef()));
useEffect(() => {
const nextHeights = elementsRef.current.map(
ref => ref.current.getBoundingClientRect().height
);
setHeights(nextHeights);
}, []);
return (
<div>
{data.map((item, index) => (
<div ref={elementsRef.current[index]} key={index} className={`item item-${index}`}>
{`${item.text} - height(${heights[index]})`}
</div>
))}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<Component />, rootElement);
.item {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ccc;
}
.item-0 {
height: 25px;
}
.item-1 {
height: 50px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"/>
I created a tiny npm package that exposes a React Hook to handle setting and getting refs dynamically as I often run into the same problem.
npm i use-dynamic-refs
Here's a simple example.
import React, { useEffect } from 'react';
import useDynamicRefs from 'use-dynamic-refs';
const Example = () => {
const foo = ['random_id_1', 'random_id_2'];
const [getRef, setRef] = useDynamicRefs();
useEffect(() => {
// Get ref for specific ID
const id = getRef('random_id_1');
console.log(id)
}, [])
return (
<>
{/* Simple set ref. */}
<span ref={setRef('random_id_3')}></span>
{/* Set refs dynamically in Array.map() */}
{ foo.map( eachId => (
<div key={eachId} ref={setRef(eachId)}>Hello {eachId}</div>))}
</>
)
}
export default Example;
You have to use a separate set of hooks for each item, and this means you have to define a component for the items (or else you’re using hooks inside a loop, which isn’t allowed).
const Item = ({ text }) => {
const ref = useRef()
const [ height, setHeight ] = useState()
useLayoutEffect(() => {
setHeight( ref.current.getBoundingClientRect().height )
}, [])
return <div ref={ref}>{text}</div>
}