Image loading in react "with refs" vs "with state" - javascript

What I want to do is, I've an Image component which shows some loading state as placeholder colour (via css background-colour) till image loads then swap it with the actual image.
Css
.show-img {
opacity: 1;
transition: all 0.3s ease-in;
}
.hide-img {
background-color:#eee;
opacity: 0;
transition: all 0.3s ease-in;
}
Using state
const Image = () => {
const [loading, setLoading] = useState(false);
const onImgLoad = () => {
setLoading(true);
};
return (
<img
src="https://i.picsum.photos/id/498/200/300.jpg"
className={loading ? 'hide-img' : 'show-img'}
onLoad={onImgLoad}
/>
);
};
Using refs
const Image = () => {
const ref = useRef();
const onImgLoad = (e) => {
//if img is loaded
if (e.target.src && ref.current) {
ref.current?.classList?.remove('hide-img');
ref.current?.classList?.add('show-img');
}
};
return (
<img
ref={ref}
src="https://i.picsum.photos/id/498/200/300.jpg"
className="hide-img"
onLoad={onImgLoad}
/>
);
};
I want to know which one of the approach is more performant and why? I was thinking about avoiding re-rendering due to state update for such a basic task (maybe).
PS: I've this Image component inside the carousel and there are multiple carousels on the page.
Thank you,

Theoretically speaking, I would argue that using state is more performant as the state is handled by react and its virtual DOM which is way faster than updating the DOM directly with useRef. Additionally, using state warranties the component re-rendering but optimised by react, which means only when its needed. I used the following articles to reach to this conclusion.
https://medium.com/swlh/useref-explained-76c1151658e8
https://blog.logrocket.com/usestate-vs-useref/
Nonetheless you may have to implement a profiler to check if this is true, as for the final user it may not look as the fastest solution, due to other circumstances outside react, like the amount of images, the connection speed and whether the images are in a CDN or not, just to mention some.

Related

Matching setState interval with css animation

I am rotating the items in an array of images inside an interval of 5 seconds. Then I have a css animation with styled components that eases in the gallery images with a fade that occurs at the same time interval.
const fadeIn = keyframes`
5%, 95% { opacity: 1 }
100% { opacity: 0 }
`
export const Gallery = styled.div<{ lapse: number }>`
position: relative;
margin: 0 auto;
max-width: 1064px;
opacity: 0;
animation: ${fadeIn} ease-in-out ${({ lapse }) => `${lapse}s`} infinite;
`
The problem is that when I change the state even thou at first it seems in sync, eventually the setState takes a bit longer
import React, { useState, useEffect } from 'react'
const [images, setImages] = useState<string[]>([])
// Time in seconds for each image swap, fading in between
const lapse = 5
...
useEffect(() => {
// Clone the images array
const imgs = [...images]
// Time interval same as css animation to fade in
const interval = setInterval(() => {
// Take the first element and put it at the end
imgs.push(...imgs.splice(0, 1))
// Update the state, this seems to desync as time passes
setImages(imgs)
}, lapse * 1000)
return () => clearInterval(interval)
}, [images])
return (
<Gallery lapse={lapse}>
<Portal>
<img src={imgs/${images[0]}`}
</Portal>
<Thumbnails>
<Thumbwrapper>
<img src={imgs/${images[1]}`}
</Thumbwrapper>
<Thumbwrapper>
<img src={imgs/${images[2]}`}
</Thumbwrapper>
</Thumbnails>
</Gallery>
)
Is there a way I can make sure the swapping happends smoothly?
Make the component class based,
constructor(props) {
super(props);
this.state = {
interval: null,
images: ["1.jpg", "2.jpg", "3.jpg"],
timeChanged: null
};
}
// setInterval when component is mounted
componentDidMount() {
var interval = setInterval(()=> {
// use callback argument for setState
// when new value (images) depends on old value
this.setState((state)=>({
timeChanged: (new Date()).toString(),
images: state.images.slice(1).concat(state.images[0])
// add 1st img at the end
});
}, 3000);
this.setState({interval: interval});
}
// clearInterval before component is unmounted
componentWillUnmount() {
clearInterval(this.state.interval);
}
When the state is updated using its old value the callback argument should be used.
(See https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous)
slice returns a portion of an array.
Also see how to update state arrays in React.
If you still wish to use hooks. Just call setImages with
(images)=>images.slice(1).concat(images[0]).
I'm not sure where the clearTimeout should be, when using hooks.
It would be great if we can have example in codesandbox. Also I would like to ask you, maybe I am missing something, but how do you determine when to stop using effect, since it depends on image, which is, I assume, part of the useSate hook, and in that same effect you are updating that same image, which will lead to another effect usage(leading to infinity loop)?
UPDATED ANSWER
I made similar example on codesandbox to look it on my own. It looks to me that main problem here is sync between applied CSS and actual update in your react component. When using setInterval with 5000ms it does not mean that it will be triggered right after 5000ms, but CSS animation on the other side is always right on time, so it stuck when two of those become unsynced.
It looks like the only way to solve this is to recreate img elements on each reorder, by introducing timestamp state and use it as part of the each img key in order to force img recreation on each update. Also you would need to change animation in simpler way.
CSS would besomething like this:
#keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.image {
position: relative;
margin: 0 auto;
max-width: 1064px;
animation: fadeIn 3s;
opacity: 1;
}
And functional component would be something like this(hook part only):
const [images, setImages] = useState(["1.jpg", "2.jpg", "3.jpg"]);
const [dateTimeWhenChanged, setDatetimeWhenChanged] = useState("");
useEffect(() => {
const imgs = [...images];
// Time interval same as css animation to fade in
const interval = setInterval(() => {
// Take the first element and but it to the end
imgs.push(...imgs.splice(0, 1));
// Update the state, this seems to desync as time passes
setImages(imgs);
setDatetimeWhenChanged(new Date().toString());
}, 5 * 1000);
return (
<div>
{images.map((img, ind) => (
<img
key={`${ind}-${dateTimeWhenChanged}`}
alt={img}
src={`images/${img}`}
className="imgg"
/>
))}
</div>
);
I came to the conclusion that having two 'clocks' to keep the time, one being react setState and the other css animation, was at the core of the problem.
So I am now keeping a single interval with a setTimeout to make sure it goes in order then substract the time taken on the timeouts from the interval and toggle the class for css
export const Gallery = styled.div<{ fadeIn: boolean }>`
position: relative;
margin: 0 auto;
max-width: 1064px;
transition: opacity 0.3s;
opacity: ${({ fadeIn }) => (fadeIn ? 1 : 0)};
`
import React, { useState, useEffect } from 'react'
const [images, setImages] = useState<string[]>([])
const [fadeIn, setFadeIn] = useState<boolean>(true)
useEffect(() => {
let interval: ReturnType<typeof setInterval>
if (images.length) {
interval = setInterval(() => {
setFadeIn(false)
setTimeout(() => {
setImages(images => images.slice(1).concat(images[0]) )
setFadeIn(true)
}, 400)
}, (lapse - 0.4) * 1000)
}
return () => clearInterval(interval)
}, [images])
const getImage = (i: number) => <img src={imgs/${images[i]}`} />
<Gallery fadeIn={fadeIn}>
<Portal>{getImage(0)}</Portal>
<Thumbnails>
<Thumbwrapper>{getImage(1)}</Thumbwrapper>
<Thumbwrapper>{getImage(2)}</Thumbwrapper>
</Thumbnails>
</Gallery>
I did however used #Nice Books better setState aproach

Loading a gif then transition to the web page on reactjs

I have a react js application when loading a page there is a gif loading at the beginning. I want a way so that the whole website is fully loaded and then the gif will stop and a transition will be made to show a ready website, this transition can be fade or growing circle, currently the gif is hardcoded with a timeout is there a way to do this ?
currently this is the code responsible for loading the gif :
The loader.js file :
import loder from "../assets/gifs/loading.gif"
const Loader = () => {
return (
<div
style={{
background: "#F7D036",
height:'100%',
width:"100%"
}}
>
<br/><br/><br/><br/><br/><br/><br/><br/><br/>
<center>
<img src={loder} height="200" alt=""/></center>
</div>
)
}
The app.js file :
useEffect(() => {
getAllCategories();
setTimeout(() => setLoading(false), 2000);
}, [getAllCategories]);
return (
<Router>
{loading == true ? (
<Loader />
) : (
some code )
we can use the approach of window.onload() event listener, in your case the execution would be like, And also you need to use states to manage them effectively
window.onload = (e) => {
// your state changes here
setLoading(false);
}
The window.onload listener will wait for the entire page to load, and you have to initiate the loader first, and then toggle this function using the useEffect like
useEffect(()=>{
//
setLoading(true);
// wrap the window.onload event inside a function and call it here
windowLoadFunction();
}, [])
Hello max, as per the comment,
so what we can do is that have your loader initiated like you have to set the state of loader as true by default, so initially your page will start loading,
So you can use the useState property to set it to true by deault, and add a css class to the loader and also add an id so we can fetch it with document.getElementById(), and so as stated in the answer, when we toggle the loader state to false, we can change the class of the loader by accessing it like document.getElementById("loader").className = "your animated css fade out class", and that's it, so, if you want your loader to fade away first then call the class change method first and then you can call the setLoader class to false,
const [loading, setLoader] = useState(true);
useEffect(()=>{
window.onload = ()=>{
// changing class of the gif to fade out using css
// change the time in this setTimeout function so that is it triggered as soon as your animation ends, match it with the animation duration, and have it started like 20 milli seconds before your animation ends,
document.getElementById("loader").className = "fade-out-loader";
setTimeout(()=>{
//this will make sure the loader is not showed anymore, and your main content will popup
setLoader(false)
}, 60)
// match time of the loader out transition so the transition will be smooth
}
},[])
{!loading}?
<div>Main content</div>:
<div class = "loader-class" id = "loader">Loader element</div>
Your css class should look like (App.css)
.loader-class{
dispaly:visible;
}
.fade-out-loader{
//add an fade animation to this class, you can get them at Animista
}

React infinite scroll component

I am trying to make application similar to facebook. When I reach bottom of the page I am fetching more data from API. I am using React and Redux.
Simplified code:
const detectBottomOfPage= () => {
if((window.innerHeight + window.scrollY) > document.body.scrollHeight) {
dispatch(bottomOfPage()); //increases scrollCount
}
};
useEffect(() => {
window.addEventListener('scroll', detectBottomOfPage);
}, []);
useEffect(() => {
dispatch(fetchMoreData(scrollCount));
},[scrollCount]);
When i scroll down it works fine, and fetches data. (page increases in height). But when i refresh the page, it detects bottom of page multiple times. I tried to remove listener before refresh (beforeunload event) but it doesn't work properly after refresh. Any solution ?
Instead of fighting with all the different scrolling quirks, the sanest solution is to ratelimit how fast you can trigger the infinite scrolling.
One way to do it would be to update your effect with some state checking:
const [lastFetch, setLastFetch] = React.useState(0);
useEffect(() => {
const now = Math.floor(Date.now()/3000);
if (lastFetch < now) {
dispatch(fetchMoreData(scrollCount));
setLastFetch(now);
}
},[scrollCount]);

How do I (correctly) check whether an element is within the viewport in React?

In a functional react component, I'm trying to check whether a call to action button (a different component) is within the viewport. If it's not, I want to display a fixed call to action button at the bottom of the viewport, which shows/hides, depending on whether the other button is visible.
I can do this using a combination of Javascript and react hooks, but although the code works in some components in my app, it doesn't work in others; I'm guessing due to react lifecycles.
I'm also aware that this is NOT the way I should be doing things in react, so would prefer to achieve the same result, but in a proper 'react way'.
I've been looking at using refs, but ideally wanted to avoid having to change my functional component to a class, as I'd like to use react hooks for the show/hide of the fixed cta. However, if this is a requirement in order to get the functionality I want, I could go for that.
Here's what I've got so far - basically, I want to replace document.querySelector with a react method:
useEffect(() => {
const CTA = document.querySelector('#CTANextSteps');
const ApplyStyle = () => (isInViewport(CTA) ? setVisible(false) : setVisible(true));
ApplyStyle();
window.addEventListener('scroll', ApplyStyle);
window.addEventListener('resize', ApplyStyle);
return function cleanup() {
window.removeEventListener('scroll', ApplyStyle);
window.removeEventListener('resize', ApplyStyle);
};
});
const isInViewport = (elem) => {
const bounding = elem.getBoundingClientRect();
return (
bounding.top >= 0 &&
bounding.left >= 0 &&
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};
As mentioned above, this function works in some areas of the app without issue, but doesn't in others; I get a Cannot read property 'getBoundingClientRect' of null error. I was surprised it worked at all, but rather than tinkering with it to try and get it working everywhere, I want to rewrite it properly.
As always, any assistance would be much appreciated. Thanks.
I was able to do it with the depedency react-visibility-sensor#5.1.1
I followed the tutorial in this link and it worked fine with me.
I don't know if this is the correct way to do it, but it works!
Here is the link https://www.digitalocean.com/community/tutorials/react-components-viewport-react-visibility-sensor
I'll put an example just in case the previous link ever goes out.
import React, { Component } from 'react';
import VisibilitySensor from 'react-visibility-sensor';
class VisibilitySensorImage extends Component {
state = {
visibility: false
}
render() {
return (
<VisibilitySensor
onChange={(isVisible) => {
this.setState({visibility: isVisible})
}}
>
<img
alt={this.props.alt}
src={this.props.src}
style={{
display: 'block',
maxWidth: '100%',
width: '100%',
height: 'auto',
opacity: this.state.visibility ? 1 : 0.25,
transition: 'opacity 500ms linear'
}}
/>
</VisibilitySensor>
);
}
}
export default VisibilitySensorImage;

ReactJS ReactCSSTransitionGroup only animating on initial load

I am trying to set up animated transitions on route change within my React / Redux app. I have been using examples on SO and the official tutorials as a guide but have so far only been able to get the fade in effect to work on the first load. When changing routes, the components are displayed without any animation in or out.
Am I missing something obvious- and is this the best path to follow when trying to animate the content in / out?
/*
** Loop through ACF components to layout page
*/
blocks = () => {
if (this.props.pages.pages[0]) {
return this.props.pages.pages[0].acf.components &&
this.props.pages.pages[0].acf.components.map((block, index) => {
let Component = componentMapping[block.acf_fc_layout];
return <Component key={index} data={block} id={index} />;
});
}
};
/*
** Render page on state change
*/
render() {
return (
<div className="App">
<NavContainer />
<ReactCSSTransitionGroup
transitionName="example"
transitionAppear={true}
transitionAppearTimeout={500}
transitionEnterTimeout={500}
transitionLeaveTimeout={300}
>
{this.blocks()}
</ReactCSSTransitionGroup>
</div>
);
}
The css I have included is as follows :
.example-enter {
opacity: 0.01;
}
.example-enter.example-enter-active {
opacity: 1;
transition: opacity 500ms ease-in;
}
.example-leave {
opacity: 1;
}
.example-appear {
opacity: 0.01;
}
.example-appear.example-appear-active {
opacity: 1;
transition: opacity .5s ease-in;
}
.example-leave.example-leave-active {
opacity: 0.01;
transition: opacity 300ms ease-in;
}
I appreciate any help that anybody could give. Thanks in advance!
Let's go through some troubleshooting steps to get to the bottom of this, but first, let me give you some basic tips on how the ReactCSSTransitionGroup component works.
<ReactCSSTransitionGroup/> needs to be present on the page with it's children being added and removed after mount. e.g. You have a list of employees that you are filtering based on an input box. As the filter controls what's being added and removed those items are coming and going and being animated. The <ReactCSSTransitionGroup/> must exist first and the component must be mounted.
With React Router, you're mounting and unmounting components as you change routes. If the <ReactCSSTransitionGroup/> is inside each component that is being mounted and unmounted the transition will not fire. I think this is what you are experiencing. I would suggest trying out this NPM package to help you take care of animations with route changes.
The issue with your animations only firing once is a separate issue than above. I'm concerned about this part:
return this.props.pages.pages[0].acf.components &&
this.props.pages.pages[0].acf.components.map((block, index) => {
let Component = componentMapping[block.acf_fc_layout];
return <Component key={index} data={block} id={index} />;
});
Returning a variable with the && operator can give you funny results. Try doing some explicit if statements. Or honestly just change your code to this:
if (this.props.pages.pages[0]) {
return this.props.pages.pages[0].acf.components.map((block, index) => {
let Component = componentMapping[block.acf_fc_layout];
return <Component key={index} data={block} id={index} />;
});
}
Another thought I had was to see what happens when you omit that component creation (componentMapping[block.afc_fc_layout]) and just return a <div>. See what happens and see if that's what's causing you trouble.

Categories

Resources