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>
Related
I've implemented face-API in my react project which is detecting a single face with detectSingleFace from the picture.
Now I want to move one step further. I want face-api to auto-crop the face after detection. So, I can store it in some server, state or local storage. Is there any way to do so?
Here you can see a screenshot example I want to achieve One side is a picture another side is the auto cropped face(which I want to implement).
Here is my live code link in codesandbox
Below is my code module for face-api
PhotoFaceDetection.js
import React, { useState, useEffect, useRef } from "react";
import * as faceapi from "face-api.js";
import Img from "./assets/mFace.jpg";
import "./styles.css";
const PhotoFaceDetection = () => {
const [initializing, setInitializing] = useState(false);
const [image, setImage] = useState(Img);
const canvasRef = useRef();
const imageRef = useRef();
// I want to store cropped image in this state
const [pic, setPic] = useState();
useEffect(() => {
const loadModels = async () => {
setInitializing(true);
Promise.all([
// models getting from public/model directory
faceapi.nets.tinyFaceDetector.load("/models"),
faceapi.nets.faceLandmark68Net.load("/models"),
faceapi.nets.faceRecognitionNet.load("/models"),
faceapi.nets.faceExpressionNet.load("/models")
])
.then(console.log("success", "/models"))
.then(handleImageClick)
.catch((e) => console.error(e));
};
loadModels();
}, []);
const handleImageClick = async () => {
if (initializing) {
setInitializing(false);
}
canvasRef.current.innerHTML = faceapi.createCanvasFromMedia(
imageRef.current
);
const displaySize = {
width: 500,
height: 350
};
faceapi.matchDimensions(canvasRef.current, displaySize);
const detections = await faceapi.detectSingleFace(
imageRef.current,
new faceapi.TinyFaceDetectorOptions()
);
const resizeDetections = faceapi.resizeResults(detections, displaySize);
canvasRef.current
.getContext("2d")
.clearRect(0, 0, displaySize.width, displaySize.height);
faceapi.draw.drawDetections(canvasRef.current, resizeDetections);
console.log(
`Width ${detections.box._width} and Height ${detections.box._height}`
);
setPic(detections);
console.log(detections);
};
return (
<div className="App">
<span>{initializing ? "Initializing" : "Ready"}</span>
<div className="display-flex justify-content-center">
<img ref={imageRef} src={image} alt="face" crossorigin="anonymous" />
<canvas ref={canvasRef} className="position-absolute" />
</div>
</div>
);
};
export default PhotoFaceDetection;
After doing a lot of R&D I figured it out. For future readers who may face an issue here is the guide.
I've created another function that will get the original image reference and the bounded box dimension i.e. width and height. After that, I've used faceapi method to extract faces and then with the help of the toDataURL method I actually converted it to base64 file which can be rendered to any image src or can be stored anywhere.
This is the function I was explaining above
async function extractFaceFromBox(imageRef, box) {
const regionsToExtract = [
new faceapi.Rect(box.x, box.y, box.width, box.height)
];
let faceImages = await faceapi.extractFaces(imageRef, regionsToExtract);
if (faceImages.length === 0) {
console.log("No face found");
} else {
const outputImage = "";
faceImages.forEach((cnv) => {
outputImage.src = cnv.toDataURL();
setPic(cnv.toDataURL());
});
// setPic(faceImages.toDataUrl);
console.log("face found ");
console.log(pic);
}
}
Then I call the above function inside my main function where I used faceapi face detection tiny model.
extractFaceFromBox(imageRef.current, detections.box);
You can also visit live code here to check complete implementation
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 .
I'm trying to add a rotation to an element when clicked. Each time I want to click on the element the element should rotate. I'm rotating the element using CSS transform property. Bellow is my styled component:
const IconButtonWrapper = styled.div`
transform: rotate(0deg);
overflow: hidden;
transition: all 0.3s ease-out;
${({ rotate }) => rotate && `transform: rotate(360deg)`};
`;
And I have the React component that renders the element and has the onClick event:
const IconButtonContainer = () => {
const [rotate, setRotate] = useState(false);
const handleClick = () =>
setRotate(
(prevState) => ({ rotate: !prevState.rotate }),
() => console.log(this.state.rotate)
);
return (
<IconButtonWrapper rotate={rotate} onClick={handleClick}>
<IconButton />
</IconButtonWrapper>
);
};
When I click the first time everything works as expected, but when I click the second time, doesn't work anymore. I feel I should return to default state rotate: false after the transition, but how do I do that in styled components?
Here is a link to a working Sandbox
Technically rotate is already a boolean so prevState should be a boolean as well. Also you were trying to set an object to the rotate state value instead of a simple boolean.
Based on that you only need to negate prevState and not prevState.rotate as:
setRotate(prevState => !prevState)
See the code modification from !prevState.rotate to !prevState.
Also suggested read is Using the State Hook. It's a bit different than class components.
Try this out.
const handleClick = () => setRotate(!rotate);
The problem is solving by changing the code of handleClick to:
import React, { useState } from "react";
import ReactDOM from "react-dom";
import styled from "styled-components";
import "./styles.css";
const IconButton = () => <button>Icon</button>;
const IconButtonWrapper = styled.div`
transform: rotate(0deg);
overflow: hidden;
transition: all 0.3s ease-out;
${({ rotate }) => rotate && `transform: rotate(360deg)`};
`;
const IconButtonContainer = () => {
const [rotate, setRotate] = useState(false);
const handleClick = () => setRotate((prevState) => (!prevState ));
return (
<IconButtonWrapper rotate={rotate} onClick={handleClick}>
<IconButton />
</IconButtonWrapper>
);
};
function App() {
return (
<div className="App">
<IconButtonContainer />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
prevState already has the latest value of rotate but you were converting it to an object. setRotate((prevState) => (!prevState )); will do.
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 have a script that fetches random images from a database, then it shows them as the page's backgroundImage. It has also a loader.
The question is, how can I wait for the div's painting to finish before closing the loader? When div receive the background state, and finished painting, I want to close the loader.
const loader = document.querySelector('.loader');
const Main = () => {
const { bgKeys, defaultColor } = React.useContext(DataContext);
const [background, setBackground] = React.useState(null);
const fetchBackground = React.useCallback(async () => {
if (bgKeys.length) {
// Get random image from IndexedDB
const rand = bgKeys[Math.floor(Math.random() * bgKeys.length)];
const bg = await idbAction('backgrounds', 'getOne', rand);
setBackground(bg.image);
// Close the loader
loader.classList.add('loaded');
}
}, [bgKeys]);
React.useEffect(() => {
fetchBackground();
}, [fetchBackground]);
return (
<div style={{ backgroundImage: `url(${background})` }} />
);
};
Thanks to eindbaas, a fellow Redditor, and other sources I forgot where I found them, the code below works by pre-loading the image in an img element (since my div uses css and not src). Then after the image loads (onload event), it calls the closeLoader function that closes the loader when the image somewhat finished painting, inside requestAnimationFrame callbacks.
const loader = document.querySelector('.loader');
const Main = () => {
const { bgKeys, defaultColor } = React.useContext(DataContext);
const [background, setBackground] = React.useState(null);
// Close loader
const closeLoader = () => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
loader.classList.add('loaded');
console.log('loaded');
});
});
};
const fetchBackground = React.useCallback(async () => {
if (bgKeys.length) {
// Get random image from IndexedDB
const rand = bgKeys[Math.floor(Math.random() * bgKeys.length)];
const bg = await idbAction('backgrounds', 'getOne', rand);
setBackground(bg.image);
}
}, [bgKeys]);
React.useEffect(() => {
fetchBackground();
}, [fetchBackground]);
return (
<div style={{ backgroundImage: `url(${background})` }}>
<img
style={{ display: 'none' }}
onLoad={closeLoader}
src={background}
alt="Wallpaper"
/>
</div>
);
};