How can I wait until all images are loaded in React.JS?
The problem is that I do some calculations (image processing) using those images and the results are different between multiple refreshes of the browser because at certain times the calculations start before the images are 100% loaded. Is there a way to wait until 100% of all images are loaded?
I've tried something like this:
const [imagesLoaded, setImagesLoaded] = useState(false);
{images.map((image, index) => {
return <ShowImage source={image} index={index+1} key={index} onLoad={() => setImagesLoaded(index)} />
})}
const ShowImage: React.FC<{source:string, index: number, onLoad: () => void}> = ({source, index, onLoad}) => {
return (
<img src={source} width="640" height="480" id={'image' + index} alt={'image' + index} onLoad={onLoad}/>
)
}
And the 'checker':
useEffect(() => {
if(imagesLoaded === 5) {
... do something
}
}, [imagesLoaded]);
But the problem is that this is working only for the first page render, if I refresh is not working, but I refresh one more time is working again and sometimes needs more refreshes, what's the problem?
You could refactor your ShowImage component to only set imagesLoaded when the last image source URL is loaded.
function ShowImages({ urls, setImagesLoaded }) {
const onLoad = (index) => {
if (index === urls.length - 1) {
setImagesLoaded(true)
}
};
return (
<>
{urls.map((url, index) => (
<img src={url} onLoad={() => onLoad(index)} key={url} />
))}
</>
);
}
export default ShowImages;
And use the component something like this
const [imagesLoaded, setImagesLoaded] = useState(false);
...
useEffect(() => {
if(imagesLoaded) {
... do something
}
}, [imagesLoaded]);
...
<ShowImages urls={images} setImagesLoaded={setImagesLoaded}/>
Working example here
Your images might not load in the intended order. If your 4th image loads after the 5th one, then the value of imagesLoaded will be 4 at the end.
To prevent that, I would increment the value one at a time, so when all five images are loaded the value will be 5.
onLoad={() => setImagesLoaded(v => v + 1)}
Related
I am creating an app where there is a list of elements created from an array of objects, and i am rendering them in a map like this
<div className="App">
<div className="notification-list">
{
notifications.map( (({ type, text }, i ) => {
return (
<Notification
handleClick={handleCloseNotification}
key={i}
index={i}
type={type}
text={text}
/>
)} ))
}
</div>
each notification should disappear after X seconds after being rendered... I have tried several approaches for adding the setTimeout but none of them has worked so far.
I tried
useEffect(() => {
const timer = setTimeout(() =>
notifications.map(({}, index) => (
handleCloseNotification( index )
)), 10000);
return () => clearTimeout(timer);
}, [notifications]);
I also tried adding this
const timer = setInterval(() => handleClick( index ), 1000);
inside the Notification component.
I also tried this
<div className="notification-list">
{
notifications.map( (({ type, text }, i ) => {
const timer = setTimeout(() => {
handleCloseNotification( i)
console.log(i);
}, 10000);
return (
<Notification
handleClick={handleCloseNotification}
key={i}
index={i}
type={type}
text={text}
/>
)} ))
}
</div>
Since I have a list that initially renders several Notifications at the same time my expectation is that they all disappear at the same time. Yet the elements disappear one by one with an interval of 10 seconds between them.
There is a form that adds new notifications to the form, and those Notifications should each live for 10 seconds and then disappear. This part seems to be working but for the elements that are rendered initially why aren't they working as I expect?
Thanks.
I am trying to implement a search feature, using custom hooks, and retrieving data from an external API. The problem is that when the user enters an invalid search term, it immediately returns the JSX element inside the if condition. How can I delay showing the JSX element using setTimeout, so first show the user a spinner or something, then redirect them to the previous page. This is what I have:
if (!movies[0])
return (
<>
<Spinner />
<h1>NO RESULTS FOUND</h1>
</>
);
This is what i'd like to do theoretically:
if (!movies[0]) => show Spinner (for 200ms) => setTimeout(show error and redirect after 200ms, 200)
how could I implement this in react?
You can maybe try something like this :
const [loading, setLoading] = useState(true);
useEffect(() => {
let timer = setTimeout(() => {
setLoading(false);
}, 2000);
return () => { clearTimeout(timer) };
}, [];
return (
<>
{loading ? <div>Loading</div> : <div>...<div>}
</>
);
I am having trouble understanding why I cannot get images to show up in my components. I have a boolean which indicates loading, and an array that gets filled async. When I finish, I set the boolean and the component re renders. Now, I want to create a card for each item in the array and put in in a card deck (this is from react-bootstrap if that wasn't obvious). I can do this with any given boolean and array, but not with the boolean and arrays created with React.useState... Why is that and how should I go about fixing this?
I encountered this problem quite a few hours ago, and have tracked down its source to this minimal working example that still reflects what I am trying to do, but I am unsure of what to do from here.
function TestCard() {
return (
<Card>
<Card.Img src="holder.js/200x200" />
</Card>
);
}
I am trying to render the following component:
function MainComponent() {
const [boolState, setBoolState] = React.useState(false);
const [arrayState, setArrayState] = React.useState([]);
React.useEffect(() => {
setTimeout(() => {
setBoolState(true);
setArrayState([1,2,3]);
}, 2000);
});
return (
<>
{/* This works */}
{
true &&
<CardDeck>
{
[1,2,3].map(_ => {
return (
<TestCard />
);
})
}
</CardDeck>
}
{/* This doesn't, why? */}
{
boolState &&
<CardDeck>
{
arrayState.map(_ => {
return (
<TestCard />
);
})
}
</CardDeck>
}
</>
);
}
Code sandbox
So I'm building a simple react app that fetches a bunch of images and displays them as cards.
The intention is to show an info message until all the images have loaded, then removing the notice again.
const App = () => {
const [cardInfo, setCardInfo] = useContext(CardInfoContext)
useEffect(() => {
fetchData(setCardInfo)
}, [])
useEffect(() => {
const app = document.querySelector('.app')
for(const child of app.children){
app.removeChild(child)
}
const loadingNotice = document.createElement('h1')
loadingNotice.innerHTML = "Fetching data ..."
app.appendChild(loadingNotice) //<-- this never shows up
cardInfo.forEach( info => {
const img = document.createElement('img')
img.src = info.image
app.appendChild(img)
})
app.removeChild(loadingNotice)
}, [cardInfo])
return (
<>
<div className="app">
<h1>Fetching data...</h1>
</div>
</>
)};
What instead happens is the app stays blank until all the images are loaded, then shows all the images at once -- but never the loading notice.
Can I somehow "push" the loading indicator change to the UI independent of the rest of the rendering?
Another thing I tried was
const App = () => {
const [cardInfo, setCardInfo] = useContext(CardInfoContext)
useEffect(() => {
fetchData(setCardInfo)
}, [])
useEffect(() => {
const app = document.querySelector('.app')
if(!cardInfo) return
const loadingNotice = app.querySelector(".loadingNotice")
loadingNotice.style.display = 'block' //<-- this never shows up
cardInfo.forEach( info => {
const img = document.createElement('img')
img.src = info.image
app.appendChild(img)
})
loadingNotice.style.display = 'none'
}, [cardInfo])
return (
<>
<div className="app">
<h1 className="loadingNotice">Fetching data...</h1>
</div>
</>
)}
Which would be incorrect because I do need to remove all the images, at least, but even that only displayed the loading notice for a fraction of a second, then the component goes blank until all the images can be displayed.
useEffect observes when cardInfo is changed, not when the render the comes after fired. You can use useLayoutEffect instead.
...but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.
BTW, I wouldn't combine direct DOM manipulation with React to avoid issues like this (among other reasons)
Something like
const App = () => {
const [isLoadgin, setIsLoading] = useState(true)
const [cardInfo, setCardInfo] = useContext(CardInfoContext)
useEffect(() => {
fetchData(result => {
setCardInfo(result);
setIsLoading(false);
})
}, [])
return (
<>
<div className="app">
{isLoading && <h1 className="loadingNotice">Fetching data...</h1>}
{
cardInfo.map(card => <img src={card.image} />)
}
</div>
</>
)}
You need conditional rendering instead of all that DOM manipulation you are trying in the useEffect.
...
return(
<>
<div className="app">
{ !cardInfo ? <h1 className="loadingNotice">Fetching data...</h1> : <Cards info={cardInfo} /> }
</div>
</>
)
Note: I am assuming you have something like <Cards> component that displays cards details.
What i'm trying to accomplish :
after click on preview div, i want to expand it to screen size, showing some loader of my own, and when the new url is ready - show its content.
is it even possible ?
const Works = ({changed}) => {
const [works, setWorks] = useState([])
useEffect(() => {
const myWorks = workService.getWorks()
setWorks(myWorks)
}, [])
const openLink = async link => {
// await load the url and show some content
// show url content
}
return (
<Page className={styles.works} changed={changed}>
{
works.map(({id, title, link, imgUrl}, i) => {
return (
<div
key={id}
className={styles.work}
style={style}
onClick={() => handleClick(i)}>
<button onClick={() => openLink(link)}>Open App</button>
</div>
)
})
}
</Page>
)
}
you can hide the content you want till it's completely loaded and ready then display it.