There is following code:
import React, { useEffect, useState } from 'react';
const Background: React.FC = () => {
const images = [ "image1.jpg", "image2.jpg", "image3.jpg", "image4.jpg" ];
const [currentImage, setCurrentImage] = useState("")
const [imageIndex, setImageIndex] = useState(0);
const changeId = () => setImageIndex(id => id + 1);
useEffect(() => {
window.addEventListener("click", changeId);
return () => window.removeEventListener("click", changeId);
}, [])
useEffect(() => {
setCurrentImage(images[imageIndex]);
}, [imageIndex])
return (
<img
src={currentImage}
alt={"background"}
/>
);
};
export default Background;
I need to achieve smooth image switching. However, the image should first smoothly fade into black, and then a new image should appear from black. Therefore, using the transition property, I think this effect cannot be achieved.
p.s. If the click is pressed too often, interrupt the animation and change the picture abruptly.
I tried to use css animation, but for some reason the image first "blinked" and then switched.
I tried using the onTransitionEnd event, but sometimes it didn't work, and as a result, the new picture didn't appear, but only a black screen remained.
Related
Consider this scenario:
An admin panel that has a sidebar and a content.
Sidebar contains menu and each item that user clicks, a component would be shown in the content area.
The content is a Router that changes the component based on URL.
Let's say we have these components:
Page1.jsx
Page2.jsx
Page3.jsx
Each of the does some operation when gets loaded. Each does it inside useEffect.
useEffect(() => {
// some operation inside PageN.jsx that takes some time
// Here we should show a progress
}, [])
We can easily define a state in each PageN.jsx and based on that return a progress or the actual component:
const [progress, setProgress] = useState()
return progress
?
<div>Loading...</div>
:
<div>Actual component</div>
This however is inefficient for an application with 100 pages and kills DRY and also introduces a lot of inconsistencies. Each page might show a different loading component, etc.
One way is to lift-up the progress state to the parent element Router or event the App itself. Then pass it down the hierarchy.
const PageN = ({ setContentProgress }) => {
// now I can call parent's setContentProgress
But it soon gets into the famous Cannot update a component from inside the function body of a different component.
So, what is the safe approach here? What is the best practice?
I would definitely suggest you to use context API, to simply build some layout context on top of your sidebar and content area.
Recently I implemented something similar for my personal project where I decided that I want to implement linear progress bar like YouTube has (red line progressing below the header), so the only option for me was to create layout context which will handle progress and then in its children I would simply use exposed actions from context itself, in order to update progress.
I did it in this way:
interface IProps {
children: React.ReactNode;
}
const initialState = {
displayProgressBar: false,
progressPercentage: 0
};
const PROGRES_BAR_TRANSITION = 300; // ms
let timeout: undefined | NodeJS.Timeout;
const Layout = ({ children }: IProps) => {
const { pathname } = useLocation();
const [context, setContext] = useState(initialState);
const navigate = useNavigate();
useEffect(() => {
setContext(initialState);
return () => {
if (timeout) clearTimeout(timeout);
};
}, [pathname]);
const setLayoutProgressVisiblity = useCallback((isVisible: boolean) => {
if (!isVisible)
timeout = setTimeout(() => {
setContext((p) => ({ ...p, displayProgressBar: isVisible }));
}, PROGRES_BAR_TRANSITION);
else setContext((p) => ({ ...p, displayProgressBar: isVisible }));
}, []);
const setLayoutProgressPercentage = useCallback(
(percentage: number) =>
setContext((p) => ({ ...p, progressPercentage: percentage })),
[]
);
const contextActions =
useMemo<ILayoutContext>(() => ({ setLayoutProgressPercentage, setLayoutProgressVisiblity }),
[setLayoutProgressPercentage, setLayoutProgressVisiblity]);
const goToHomepage = useCallback(() => {
navigate("/", { replace: true });
}, [navigate]);
return (
<LayoutContext.Provider value={contextActions}>
<div className="app-container p-relative overflow-auto">
<div>{/** HERE YOU CAN PUT HEADER */}</div>
{context.displayProgressBar && (
<HeaderProgressBar progress={context.progressPercentage} />
)}
<div className=" py-4 flex-fill overflow-auto">
{<div className="container h-100">{children}</div>}
</div>
</div>
</LayoutContext.Provider>
);
};
export default Layout;
Beside this, I created one custom hook in order to use it whenever I want to access layout actions:
export const useLayoutContext = () => {
const context = useContext(LayoutContext);
if (!context)
throw new Error(
"No LayoutContext.Provider found when calling useLayoutContext."
);
return context;
};
And finally it is very easy to update progress within any element which is inside LayoutContext. Simple as this:
const { setLayoutProgressPercentage, setLayoutProgressVisiblity } =
useLayoutContext();
i am building a nextjs component called SocialMediaIcon = this component should recive a single lottie json path and render a lottie svg, when the user hovers on the animation it should start playing and when the mouse leaves it should reset the animation to position 0 ,
import { useEffect, useRef, useState } from 'react'
import { LottiePlayer } from 'lottie-web'
function SocialMediaIcon({ iconPath }) {
const ref = useRef(null)
const anim = useRef(null)
const [lottie, setLottie] = useState(null)
useEffect(() => {
import('lottie-web').then((Lottie) => setLottie(Lottie.default))
}, [])
useEffect(() => {
if (lottie && ref.current) {
anim.current = lottie.loadAnimation({
container: ref.current,
renderer: 'svg',
loop: true,
autoplay: false,
// path to your animation file, place it inside public folder
animationData: iconPath,
})
return () => anim.current.destroy()
}
}, [lottie])
const handleAnimation = () => {
lottie.play()
}
const handlemouseOut = () => {
lottie.goToAndStop(0)
}
return (
<div
ref={ref}
className="lottie h-20 rounded-lg bg-white p-2"
onMouseEnter={handleAnimation}
onMouseLeave={handlemouseOut}
></div>
)
}
export default SocialMediaIcon
and this is how i am rendering the component in the parrent component :
<SocialMediaIcon iconPath={githubJson} />
<SocialMediaIcon iconPath={twitterJson} />
the problem i'm having is that when i hover over one component all the components are playing the animation instead of just the one that has been hovered
i have already tried :
1.using a anim = useRef() hook and passing it to lottie.play(anim.current) which resulted in no animation being played at all
2.i also tried passing animation to lottie play lottie.play(lottie.animation) thinking every instance has its own animation and the animation did not play .
it would be greatly appreciated if you could help me understand this on a deeper level, i think im really missing a important concept not sure if its about lottie or nextjs in genral.
I have a component Scroller which I don't control which takes in data as a prop.
This data is a list of objects. Within object, one of the keys takes in a function.
This component has ability where upon clicking on the square, I am meant to show a new component (like a pop up).
The component Scroller which I don't control taking in the data prop.
<Scroller
data={getData(allData)}
/>
This is the data being passed in. content is a list of objects.
const getData = (content) => content.map((c, i) => ({
header: c.header,
customOnClick: (() => {
setClicked(true); // this is the line which resets the scroll
}),
}
));
So this works as intended. Upon clicking, the new pop up content shows. This is due to state change via the setClicked function.
The issue is that this Scroller component has a scroll option. So user could have scrolled pass a a block (0) like following image.
But the moment I click the button to show the popup, it resets the scroll position back to 0 like following. Instead of remaining in position as above.
This scroll reset is the issue.
This is being caused by the call to setClicked function. It doesn't matter if I do anything with it. As long as I call it, it resets.
Showing the popup component is not the issue. The mere call to setClicked is the issue.
Thus wondering if there a way I could toggle showing the pop up component without having to set state?
Or a way to maintain the scroll position without resetting the scroll.
Note that in this instance I am using hooks. It is the same outcome if I use Redux. Please advice.
This is my component which I can control.
import React, { Fragment } from 'react';
import Scroller from 'comp-external-lib';
import PopUpComponent from './PopUpComponent';
const MyComponent = ({data}) => {
const [isClicked, setClicked] = React.useState(false);
const { allData } = data;
const getData = (content) => content.map((c, i) => ({
header: c.header,
customOnClick: c.customOnClick && (() => {
setClicked(true); // this is whats causing the reset for scroll
}),
}
));
return (
<Fragment>
<Scroller
data={getData(allData)}
/>
{
{/* Doesn't matter if this is commented out. The scrolling will still reset due to call to setClicked function */}
{/* isClicked && <PopUpComponent /> */}
}
</Fragment>
);
};
export default MyComponent;
Explanation:
Each time setClick is called, the value of isClicked is changed, which causes MyComponent to be reevaluated. Since allData is initialized inside MyComponent, it will be reinitialized each time MyComponent is reevaluated. Another issue is that the data being sent to Scroller is the result of a function that takes in allData. Each time MyComponent is reevaluated, that function will run again and return a new array instance given the new allData instance. This means that every time MyComponent reevaluates, Scrollbar gets a new instance of data, causing anything that consumes data inside of Scrollbar to also be reevaluated.
Solution:
My suggestion would be to utilize react's useMemo hook (docs: https://reactjs.org/docs/hooks-reference.html#usememo) to 'memoize' the data going into Scroller:
import React from 'react';
import Scroller from 'comp-external-lib';
import PopUpComponent from './PopUpComponent';
const MyComponent = ({data}) => {
const [isClicked, setClicked] = React.useState(false);
const scrollerData = React.useMemo(()=> {
return data.allData.map((c, i) => ({
header: c.header,
customOnClick: c.customOnClick && (() => {
setClicked(true); // this is whats causing the reset for scroll
}),
}
));
},[data])
return (
<>
<Scroller
data={scrollerData}
/>
{
{/* Doesn't matter if this is commented out. The scrolling will still reset due to call to setClicked function */}
{/* isClicked && <PopUpComponent /> */}
}
</>
);
};
export default MyComponent;
Also fun fact, <> is shorthand for React's Fragment
The problem could be that each time you click the component your Scroller gets a different reference of the data and because of that, it calls lifecycle methods that cause your performance issue.
If you will send the same props ( same reference ) to Scroller it should not call any lifecycle method which propably causes your problems.
import React, { Fragment, useMemo, useState } from 'react'
import Scroller from 'comp-external-lib'
import PopUpComponent from './PopUpComponent'
const MyComponent = props => {
const [isClicked, setClicked] = useState(false)
const { allData } = props.data
const getData = content =>
content.map((c, i) => ({
header: c.header,
customOnClick:
c.customOnClick &&
(() => {
setClicked(true)
})
}))
const scrollerData = useMemo(() => getData(allData), [allData])
return (
<Fragment>
<Scroller data={scrollerData} />
{isClicked && <PopUpComponent />}
</Fragment>
)
}
export default MyComponent
You are calling getData on every render cycle and thus causing a reset of the state:
data={getData(allData)}
The solution will be to wrap the getData function with a useCallback hook:
const getData = useCallback((content) => content.map((c, i) => ({
header: c.header,
customOnClick: c.customOnClick && (() => {
setClicked(true); // this is whats causing the reset for scroll
}),
}
)),[]);
I am trying to apply a parallax effect to an .svg image by using useRef() to grab bubblesRef and translateY() onScroll.
The parallax works but when I navigate to the next page I receive error "TypeError: Cannot read property 'style' of null". I think it is because the addEventListener is still listening and trying to useRef() on bubblesRef while navigating to the next page. So I added the cleanup function in useEffect() but that doesn't seem to fix it.
Any help is appreciated. Thanks!
p.s. If anyone can share their approach to a simple parallax effect like this that would be great too. This is the only approach I've figured that won't rerender everything else on the page onScroll.
const HomePage = () => {
const [loadedPosts, setLoadedPosts] = useState([]);
const { sendRequest } = useHttpClient();
console.log("loadedPosts homePage", loadedPosts);
const bubblesRef = useRef();
useEffect(() => {
if (loadedPosts.length === 0) {
//api call
}
}, [sendRequest, loadedPosts]);
useEffect(() => {
const parallax = () => {
let scrolledValue = window.scrollY / 3.5;
bubblesRef.current.style.transform = `translateY(
-${scrolledValue + "px"}
)`;
console.log("scrolling...", scrolledValue);
};
window.addEventListener("scroll", parallax);
return () => window.removeEventListener("scroll", parallax);
}, []);
return (
<HomePageContainer>
<Header />
<SectionOne posts={loadedPosts} />
<SectionTwo />
<BubbleBlobs className="bubbleBlobs" ref={bubblesRef} />
<BlobTop className="backBlobBottom" preserveAspectRatio="none" />
</HomePageContainer>
);
};
export default HomePage;
You definitely need the cleanup function any time you add a listener to the window, or the handler (and thus the component instance itself) will live on forever. However, since React runs those cleanup hooks asynchronously, it might not happen until after other window events. The value of the ref is set to null when the component unmounts, so you need to check that it is still defined before using the value.
useEffect(() => {
const handler = () => {
if (ref.current) {
// perform update
}
}
window.addEventListener('scroll', handler)
return () => window.removeEventListener('scroll', handler)
}, [])
When you call useEffect, your reference has not been instantiated, so the error message appears, in your useEffect dependency array, insert your ref and before running the code in useEffect, make sure your current reference is defined.
I am currently in the process of converting a ReactJS client side rendered application to a NextJS application for search engine optimization and social media linking reasons.
One of the components converted, which is basically an image that waits until it's finished loading then fades in, is not working as expected after it is used in a NextJS environment.
It behaves in the following manner:
Cache Enabled:
The first time the image loads and the onLoad event triggers thus showing the image.
The second time the image stays hidden because the onLoad event doesn't trigger when the image is loaded from cache.
Cache Disabled using devtools:
The onLoad event always works because the image is never served from cache.
Expected behavior and the behavior previously achieved with using just ReactJS:
The onLoad event should trigger whether the image was loaded from the cache or not.
When not using React this problem is usually caused when someone sets the images src before defining a onload function:
let img = new Image()
img.src = "img.jpg"
img.onload = () => console.log("Image loaded.")
Which should be:
let img = new Image()
img.onload = () => console.log("Image loaded.")
img.src = "img.jpg"
Here is simplified code that causes the same problem in NextJS:
import React, { useState } from "react"
const Home = () => {
const [loaded, setLoaded] = useState(false)
const homeStyles = {
width: "100%",
height: "96vh",
backgroundColor: "black"
}
const imgStyles = {
width: "100%",
height: "100%",
objectFit: "cover",
opacity: loaded ? 1 : 0
}
const handleLoad = () => {
console.log("Loaded")
setLoaded(true)
}
return (
<div className="Home" style={homeStyles}>
<img alt=""
onLoad={handleLoad}
src="https://images.unsplash.com/photo-1558981001-5864b3250a69?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80"
style={imgStyles}
/>
</div>
)
}
export default Home
I ended up using ImageObject.complete as a workaround thanks to someone's suggestion.
I used useRef to reference the image and checked if the image.current.complete === true on component mount.
Here is the code:
import React, { useEffect, useRef, useState } from "react"
const Home = () => {
const [loaded, setLoaded] = useState(false)
const image = useRef()
const homeStyles = {
width: "100%",
height: "96vh",
backgroundColor: "black"
}
const imgStyles = {
width: "100%",
height: "100%",
objectFit: "cover",
opacity: loaded ? 1 : 0
}
const handleLoad = () => setLoaded(true)
useEffect(() => {
if (image.current.complete) setLoaded(true)
}, [])
return (
<div className="Home" style={homeStyles}>
<img alt=""
ref={image}
onLoad={handleLoad}
src="https://images.unsplash.com/photo-1558981001-5864b3250a69?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80"
style={imgStyles}
/>
</div>
)
}
export default Home
From Next.js v11.0.2-canary.4 onwards, one can directly use onLoadingComplete prop.
<Image src={src} onLoadingComplete={() => setLoaded(true)} />
How does this compare with other options??
You get to keep using the awesome next/image component instead of unoptimized img tag without any extra code or dealing with refs yourself.
If you are using the new Image component from nextjs 10, which does not support a forward ref, you can modify the workaround with
const [loaded, setLoaded] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if ((ref.current?.firstChild?.firstChild as HTMLImageElement | undefined)?.complete) {
setLoaded(true)
}
}, [])
return <div ref={ref}>
<Image src={src}
onLoad={() => setLoaded(true)}/>
</div>
I only want to add to #mohamed-seif-khalid answer that I had to add a check in useEffect if ref consist anything, because my render continued to break.
useEffect(() => {
if (image.current && image.current.complete) {
handleImageLoad()
}
}, []);