Event that fires after a certain element successfully painted - javascript

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

Related

react-webcam - disable buttons until stream starts

I am using the react-webcam to capture images and videos in my react app. I have implemented the Screenshot (via Ref) example:
const videoConstraints = {
width: 1280,
height: 720,
facingMode: "user"
};
const WebcamCapture = () => {
const webcamRef = React.useRef(null);
const capture = React.useCallback(
() => {
const imageSrc = webcamRef.current.getScreenshot();
},
[webcamRef]
);
return (
<>
<Webcam
audio={false}
height={720}
ref={webcamRef}
screenshotFormat="image/jpeg"
width={1280}
videoConstraints={videoConstraints}
/>
<button onClick={capture}>Capture photo</button>
</>
);
};
Works nicely, however it seems to take a few seconds for the stream to start and video to start showing. I want to be able to disable the button whilst this is happening. I have found there is a flag in the webcamRef that states if it is loading:
useEffect(() => {
if (webcamRef.current) {
const camStarted = webcamRef.current.state.hasUserMedia;
debugger;
}
}, [webcamRef]);
In the above useEffect, whilst the video is initialising, the hasUserMedia is false, once it has loaded it changes to true. This sounds like exactly what I need however as it is in a useRef, it doesn't hit the useEffect when it changes.
Is there any kind of neat trick I can implement to be able to identify when this value in the useRef is changed, or anything else that might help me get the functionality I am after?
If you just want to disable the button until the camera has started streaming, you can use a check that can be triggered using the onUserMedia prop of Webcam.
const videoConstraints = {
width: 1280,
height: 720,
facingMode: "user"
};
const WebcamCapture = () => {
const [loadingCam, setLoadingCam] = useState(true);
const webcamRef = React.useRef(null);
const capture = React.useCallback(
() => {
const imageSrc = webcamRef.current.getScreenshot();
},
[webcamRef]
);
const handleUserMedia = () => {
setTimeout(() => {
// timer is optional if the loading is taking some time.
setLoadingCam(false);
}, 1000);
};
return (
<>
<Webcam
audio={false}
height={720}
ref={webcamRef}
screenshotFormat="image/jpeg"
width={1280}
videoConstraints={videoConstraints}
onUserMedia={handleUserMedia}
/>
<button disable={loadingCam} onClick={capture}>Capture photo</button>
</>
);
};

React detect the change in iframe's src

I have an iframe element inside my component and I need to somehow detect when the URL of that iframe changes. What I have tried so far:
Added onLoad callback, but this does not trigger every time I redirect in the iframe
Used React.useCallback but also does not work as I wanted to
Create timeout and get the URL from the iframe every x seconds
Minimalistic code example below
export const XComponent = (props: XComponentProps) => {
const ref = React.useRef<any>();
1.
const onLoad = () => {
const url = ref.current.contentWindow.location.href;
// do stg with url
}
2.
const getRef = React.useCallback((node: any) => {
// store node into state, this was not triggered properly either
}, []);
3.
React.useEffect(() => {
const interval = setInterval(() => {
const url = ref.current.contentWindow.location.href;
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<div className={ styles.albertStoremanTab }>
<div className={ styles.container }>
<iframe id={"iframe-my-id"} onLoad={onLoad} src={props.defaultUrl} ref={ref}></iframe>
</div>
</div>
);
};
That's simple:
useEffect(() => {
// It runs only when defaultUrl changes
}, [props.defaultUrl])
Did you try using ref.current in the dependency array of the useEffect?
useEffect(() => { setURL(ref.current.contentWindow.location.href) },
[ref.current])

Cannot get the updated the state value useState and UseEffect

I want to get the latest value of the state and use it inline style, but it firstly returns 0 values, then rendering updated the state. However, I cannot assign the values into the style.
const ImageCard = ({ image: { alt_description, urls } }) => {
const [spans, setSpans] = useState(0);
const imageRef = useRef(null);
useEffect(() => imageRef.current.addEventListener('load', () => {
const height = imageRef.current.clientHeight;
const spans = Math.ceil(height / 10);
setSpans({ spans });
}));
return (
<div style={{ gridRowEnd: `span ${spans}` }}>
<img ref={imageRef} alt={alt_description} src={urls.regular} />
</div>
);
}
console output:
10 0
{spans: 15}
{spans: 33}
...
You would not need useEffect for this. You can use the onLoad event of the img tag as follows:
const ImageCard = ({ image: { alt_description, urls } }) => {
const [spans, setSpans] = useState(0);
const imageLoaded = e => {
const height = e.currentTarget.clientHeight;
const spans = Math.ceil(height / 10);
setSpans({ spans });
};
//Dont forget to remove this line
console.log(spans);
return (
<div style={{ gridRowEnd: `span ${spans}` }}>
<img onLoad={imageLoaded} alt={alt_description} src={urls.regular} />
</div>
);
};
Working code sandbox example

Load more implementation in ReactJs

I am trying to implement load more button for my small project GiF generator. First I thought of appending next set of 20 response at the bottom, but failed to do.
Next, I thought of implementing loading the next set of 20 results by simply removing the current one. I tried to trigger a method on click of button, but I failed to do so. Its updating the state on second click of load more and then never updating it again.
Please help me find what I am missing, I have started learning React yesterday itself.
import React, { useEffect, useState } from 'react';
import './App.css';
import Gif from './Gif/Gif';
const App = () => {
const API_KEY = 'LIVDSRZULELA';
const [gifs, setGif] = useState([]);
const [search, setSearch] = useState('');
const [query, setQuery] = useState('random');
const [limit, setLimit] = useState(20);
const [pos, setPos] = useState(1);
useEffect(() => {
getGif();
}, [query])
const getGif = async () => {
const response = await fetch(`https://api.tenor.com/v1/search?q=${query}&key=${API_KEY}&limit=${limit}&pos=${pos}`);
const data = await response.json();
setGif(data.results);
console.log(data.results)
}
const updateSearch = e => {
setSearch(e.target.value);
}
const getSearch = e => {
e.preventDefault();
setQuery(search);
setSearch('');
}
const reload = () => {
setQuery('random')
}
const loadMore = () => { // this is where I want my Pos to update with 21 on first click 41 on second and so on
let temp = limit + 1 + pos;
setPos(temp);
setQuery(query);
}
return (
<div className="App">
<header className="header">
<h1 className="title" onClick={reload}>React GiF Finder</h1>
<form onSubmit={getSearch} className="search-from">
<input className="search-bar" type="text" value={search}
onChange={updateSearch} placeholder="type here..." />
<button className="search-button" type="submit">Search</button>
</form>
<p>showing results for <span>{query}</span></p>
</header>
<div className="gif">
{gifs.map(gif => (
<Gif
img={gif.media[0].tinygif.url}
key={gif.id}
/>
))}
</div>
<button className="load-button" onClick={loadMore}>Load more</button>
</div>
);
}
export default App;
Please, help me find, what I am doing wrong, As I know the moment I will update setQuery useEffect should be called with new input but its not happening.
Maybe try something like this:
// Fetch gifs initially and then any time
// the search changes.
useEffect(() => {
getGif().then(all => setGifs(all);
}, [query])
// If called without a position index, always load the
// initial list of items.
const getGif = async (position = 1) => {
const response = await fetch(`https://api.tenor.com/v1/search?q=${query}&key=${API_KEY}&limit=${limit}&pos=${position}`);
const data = await response.json();
return data.results;
}
// Append new gifs to existing list
const loadMore = () => {
let position = limit + 1 + pos;
setPos(position);
getGif(position).then(more => setGifs([...gifs, ...more]);
}
const getSearch = e => {
e.preventDefault();
setQuery(search);
setSearch('');
}
const updateSearch = e => setSearch(e.target.value);
const reload = () => setQuery('random');
Basically, have the getGifs method be a bit more generic and then if loadMore is called, get the next list of gifs from getGift and append to existing list of gifs.

React can't trigger window.onmouseup

I have a slider component, which should stop moving after mouse is up. I have went through the forum and my code is very similar to the one here
const Slider = ({ mainColour }) => {
const [cursorPos, setCursorPos] = React.useState(0);
const [isSliding, setSliding] = React.useState(false);
const ref = React.useRef();
const drag = e => {
console.log("dragging");
setCursorPos(e.pageY);
};
const startDrag = e => {
setSliding(true);
window.addEventListener("mousemove", drag);
window.addEventListener("mouseup", function() {
ref.current.onmousedown = null;
ref.current.onmouseup = null;
ref.current.onmousemove = null;
setSliding(false);
window.onmouseup = null;
});
};
return (
<div
className={`cursor ${isSliding ? "active" : ""}`}
ref={ref}
style={{
top: `${cursorPos}px`,
backgroundColor: `${mainColour}`
}}
onMouseDown={event => startDrag(event)}
></div>
);
};
export default Slider;
However, when startDrag triggers, the window.onmouseup listener doesn't seem to be working and does not stop the slider. Will be appreciated for any insights why it doesn't work.
https://codesandbox.io/s/lucid-sunset-8e78r
React can trigger mouseup, you just need to use window.removeEventListener for drag() when you mouseup. That's why you see dragging in the console after mouseup, you just forgot to unsubscribe from the event :)
window.onmouseup = null; is not the same as window.removeEventListener("mousemove").
const Slider = ({ mainColour }) => {
const [cursorPos, setCursorPos] = React.useState(0);
const [isSliding, setSliding] = React.useState(false);
const ref = React.useRef();
const drag = e => {
console.log("dragging");
setCursorPos(e.pageY);
};
useEffect(() => {
if (isSliding) {
window.addEventListener("mousemove", drag);
}
}, [isSliding]);
useEffect(() => {
window.addEventListener("mouseup", function() {
window.removeEventListener("mousemove", drag);
setSliding(false);
});
});
const startDrag = () => setSliding(true);
return (
<div
className={`cursor ${isSliding ? "active" : ""}`}
ref={ref}
style={{
top: `${cursorPos}px`,
backgroundColor: `${mainColour}`
}}
onMouseDown={event => startDrag(event)}
/>
);
};
I agree with the comment from artanik, but with a very slight change. Instead of using the useEffect without any dependencies, and constantly adding and removing event listeners from the window object, I would rather only set and unset it when the isSliding changes value. Also it seems that the ref is not used anywhere, so I presume instead of using the window object you could set it only for the element in the ref.
The purpose of triggering the useEffect with an empty array once is to not run it every render. Imagine a component that would have a lot of state changing and data going through it, adding and removing a bunch of event listeners in one go in every render is not needed.
const Slider = ({ mainColour }) => {
const [cursorPos, setCursorPos] = React.useState(0);
const [isSliding, setSliding] = React.useState(false);
///only do this once, when the component mounts
useEffect(() => {
window.addEventListener("mousedown", startDrag);
window.addEventListener("mouseup", endDrag);
},[]);
//and setting and unseting the event as needed
useEffect(() => {
if (isSliding) {
window.onmousemove = handleDrag;
}
else{
window.onmousemove = null;
}
}, [isSliding]);
const startDrag = () => setSliding(true);
const endDrag = () => setSliding(false);
const handleDrag = (e) =>{
console.log("dragging");
setCursorPos(e.pageY);
}
return (
<div
className={`cursor ${isSliding ? "active" : ""}`}
style={{
top: `${cursorPos}px`,
backgroundColor: `${mainColour}`
}}
/>
);
};

Categories