React: How do you lazyload image from API response? - javascript

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))
)
}
/>
));
}

Related

Check if element is outside window React

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>

lazy loading for image in styled component

const BackgroundImage = styled.div`
background: url(${(props) => props.backgroundImage}) no-repeat center center;
}
I use div in style component, but it has flickering issue waiting for the image to come in. Is there any lazy loading solution using styled-component's div?
Create a wrapper around your styled component which doesn't show the div until the image is loaded.
You can accomplish this by creating an temporary image, wait for it to load, and then tell the component the image is ready and can be displayed.
const BackgroundImage = styled.div`
background: url(${(props) => props.backgroundImage}) no-repeat center center;
`;
const LazyBackgroundImage = ({ src, children }) => {
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const image = new Image();
image.addEventListener('load', () => setIsLoaded(true));
image.src = src;
}, [src]);
if (!isLoaded) {
return null;
}
return (
<BackgroundImage backgroundImage={src}>
{children}
</BackgroundImage>
);
};
Use the wrapper like this:
<LazyBackgroundImage src="path/to/your-image.jpg">
<p>Hi there</p>
</LazyBackgroundImage>

In React, how can I calculate the width of a parent based on it's child components?

I am working on a draggable carousel slider in React and I'm trying to figure out how to calculate the total width of the containing div based on the number of child elements it contains. I assumed I would be able to get the width after the component mounted but it didn't return the result I was expecting...I now assume it needs to wait for the images to be loaded and mounted to do this. What would be the best way to get the width of the <div className="carousel__stage">?
const Carousel = () => {
const carouselStage = useRef(null);
useEffect ( () => {
console.log(carouselStage.scrollWidth);
}, [carouselStage]);
return (
<div className="carousel">
<div ref={carouselStage} className="carousel__stage">
<Picture />
<Picture />
<Picture />
<Picture />
<Picture />
</div>
</div>
);
}
Sounds like a use case for ResizeObserver API.
function useResizeObserver() {
const [size, setSize] = useState({ width: 0, height: 0 });
const resizeObserver = useRef(null);
const onResize = useCallback((entries) => {
const { width, height } = entries[0].contentRect;
setSize({ width, height });
}, []);
const ref = useCallback(
(node) => {
if (node !== null) {
if (resizeObserver.current) {
resizeObserver.current.disconnect();
}
resizeObserver.current = new ResizeObserver(onResize);
resizeObserver.current.observe(node);
}
},
[onResize]
);
useEffect(
() => () => {
resizeObserver.current.disconnect();
},
[]
);
return { ref, width: size.width, height: size.height };
}
Pass a callback function to each Pictures and use useState hook to calculate total width.
const [width, setWidth] = useState(200); //default width
const handleImageLoaded = (size) =>{
setWidth(width + size)
}
Use ref to get image's width like you already did and pass it to the callback function .

How to move state outside of component using context provider

I currently have a preview component which has a reloading functionality attached into it using the useState hook. I now want the ability to refresh this component with the same functionality but with an external component. I know that this can be achieved by the useContext API, however i'm struggling to plug it all together.
Context:
const PreviewContext = React.createContext({
handleRefresh: () => null,
reloading: false,
setReloading: () => null
});
const PreviewProvider = PreviewContext.Provider;
PreviewFrame:
const PreviewFrame = forwardRef((props, ref) => {
const { height, width } = props;
const classes = useStyles({ height, width });
return (
<Card className={classes.root} ref={ref}>
<div className={classes.previewWrapper} > {props.children} </div>
<div className={classes.buttonContainer}>
<IconButton label={'Refresh'} onClick={props.toggleReload} />
</div>
</Card>
);
});
PreviewFrameWrapped:
<PreviewFrame
toggleReload={props.toggleReload}
height={props.height}
width={props.width}
ref={frameRef}
>
<PreviewDiv isReloading={props.isReloading} containerRef={containerRef} height={height} width={width} />
</PreviewFrame>
const PreviewDiv = ({ isReloading, containerRef, height, width }) => {
const style = { height: `${height}px`, width: `${width}px`};
return !isReloading ?
<div className='div-which-holds-preview-content' ref={containerRef} style={style} />
: null;
};
Preview:
export default function Preview(props) {
const [reloading, setReloading] = useState(false);
useEffect(() => {
setReloading(false);
}, [ reloading ]);
const toggleReload = useCallback(() => setReloading(true), []);
return <PreviewFrame isReloading={reloading} toggleReload={toggleReload} {...props} />
}
So now i want to just be able to import the preview component and be able to refresh it using an external button, so not using the one that's already on the <PreviewFrame>.
I ideally want to consume it like this:
import { PreviewContext, PreviewProvider, Preview } from "../../someWhere"
<PreviewProvider>
<Preview />
<PreviewControls />
</PreviewProvider>
function PreviewControls () {
let { handleRefresh } = React.useContext(PreviewContext);
return <div><button onClick={handleRefresh}>↺ Replay</button></div>
}
Preview With My Attempt at Wrapping with Provider:
export default function Preview(props) {
const [reloading, setReloading] = useState(false);
useEffect(() => {
setReloading(false);
}, [ reloading ]);
const toggleReload = useCallback(() => setReloading(true), []);
return (<PreviewProvider value={{ reloading: reloading, setReloading: setReloading, handleRefresh: toggleReload }} >
<PreviewFrame isReloading={reloading} toggleReload={toggleReload} {...props} />
{/* it works if i put the external button called <PreviewControls> here*/}
</PreviewProvider>
);
}
So yeah as i said in the commented out block, it will work if put an external button there, however then that makes it attached/tied to the Preview component itself, I'm really not sure how to transfer the reloading state outside of the Preview into the Provider. Can someone please point out what i'm missing and what i need to do make it work in the way i want to.
All you need to do is to write a custom component PreviewProvider and store in the state of reloading and toggleReload function there. The preview and previewControls can consume it using context
const PreviewContext = React.createContext({
handleRefresh: () => null,
reloading: false,
setReloading: () => null
});
export default function PreviewProvider({children}) {
const [reloading, setReloading] = useState(false);
useEffect(() => {
setReloading(false);
}, [ reloading ]);
const toggleReload = useCallback(() => setReloading(true), []);
return <PreviewContext.Provider value={{reloading, toggleReload}}>{children}</PreviewContext.Provider>
}
export default function Preview(props) {
const {reloading, toggleReload} = useContext(PreviewContext);
return <PreviewFrame isReloading={reloading} toggleReload={toggleReload} {...props} />
}
function PreviewControls () {
let { toggleReload } = React.useContext(PreviewContext);
return <div><button onClick={toggleReload}>↺ Replay</button></div>
}
Finally using it like
import { PreviewContext, PreviewProvider, Preview } from "../../someWhere"
<PreviewProvider>
<Preview />
<PreviewControls />
</PreviewProvider>

How to add "refs" dynamically with react hooks?

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>
}

Categories