NextJS: Images loaded from cache don't trigger the onLoad event - javascript

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()
}
}, []);

Related

manage a single instance of a Nextjs Component seperatly for lottie Animations?

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.

How to test condition rendered elements with react testing library

I'm trying to cover my functionality with test, i have the image input:
<input
data-testid="fileuploadtestid"
type="file"
accept="image/*"
capture="camera"
onChange={uploadImage}
/>
On change method checks if file provided and creates the link;
const uploadImage = (e) => {
const { files } = e.target;
if (files?.length) {
const url = URL.createObjectURL(files[0]);
setImageUrl(url);
} else {
setImageUrl("");
}
};
Based on imageUrl value image renders conditionally:
{imageUrl && (
<img
data-testid="prevtestid"
className="preview_img"
src={imageUrl}
alt={imageUrl}
crossOrigin="anonymous"
ref={imageRef}
/>
)}
And i want to check if it renders correctly using react testing library to get the image element I use data-testid="prevtestid":
test("renders learn react link", async () => {
const { container, rerender } = render(<Home />);
global.URL.createObjectURL = jest.fn();
const file = new File(["(⌐□_□)"], "pug.png", { type: "image/png" });
const input = screen.getByTestId("fileuploadtestid");
fireEvent.change(input, { target: { files: [file] } });
rerender(<Home />);
expect(screen.getByTestId("prevtestid")).toBeInTheDocument();
}
);
I attached the latest version what have i tried to implement, i tried to use waitFor, rerender, render in various ways but i'm getting the same error all time:
Unable to find an element by: [data-testid="prevtestid"].
Please tell me how can i test it ?
The mock that you are creating is not really returning anything. You should return a url so that the components get's an imageUrl and is then able to show the <img /> element.
global.URL.createObjectURL = jest.fn().mockReturnValueOnce('https://www.samplerandomdomain.com/images/123.jpg');

Get scrollbar position with NextJs

I'm using NextJs to take advantage of Server Side Rendering. And also I have a navbar in my application that should change styles with scroll position. How can I check whether the window has been scrolled more than 100px, on my NextJs application?
You can simply use a useEffect hook like this:
import { useEffect, useState } from "react";
const IndexPage = () => {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
};
// just trigger this so that the initial state
// is updated as soon as the component is mounted
// related: https://stackoverflow.com/a/63408216
handleScroll();
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div style={{ height: 4000 }}> {/* just added to make scrollbar available */}
<div style={{ position: "fixed", top: 0 }}>
{scrollY > 100
? "Scrolled more than 100px"
: "Still somewhere near the top!"}
</div>
</div>
);
};
export default IndexPage;
Sandbox: https://codesandbox.io/s/cocky-drake-1xe0g
This code can be further optimized by debouncing the scroll handler. And optionally, setting the state only if it is not changed (not sure if newer versions of React handle this itself).
Your question was directly related to this thread: https://stackoverflow.com/a/59403018/11613622
For debouncing/throttling you can refer this: How to use throttle or debounce with React Hook?
Also if you don't want to use the provided solutions in that thread simply wrap the handleScroll with _.debounce and then feed it to the event handler.

Facing Problems in Converting to Functional Components in React

This is a very small two-page project, so this won't take much time.
I am trying to convert this Github repo from class-based component to functional component. I am very close but the logic is just not working properly.
The useState hook especially, as in the values are not getting saved. So I tried a different approach.
This is the expected output which is the live demo of the original project:
https://matt-eric.github.io/web-audio-fft-visualization-with-react-hooks/
And this is where I am. This is the sandbox link.
Ignore the The error you provided does not contain a stack trace. error for now. Click on the x and refresh the small project window (not your browser tab) a couple of times until the audio plays on refresh. This is because google stops you from playing music on load.
I want to audio to play with the click of the button and not on load. But it is not working.
Thank whoever goes and looks into it.
There's some cleanup needed but this is working. https://codesandbox.io/s/xenodochial-wildflower-c5b35
All I really did was put all the functions in a useCallback made audioFile a ref, and then made the toggleAudio function which either plays or pauses the audio depending on its current state. One of the biggest problems I saw was that you were trying to initialize the audio on click, but that really should be done on mount, then the audio just starts when you click. Also if you initialize on every click then it causes errors because it's already initialized.
Let me know if you have any questions!
Using your sandbox, I found a couple of things missing:
You needed to memoize your audoFile (since you create a new audio and it never changes)
Your functions need to be stable, and therefore need to be react hook functions, specifically useCallback functions.
In your onClick function in the demo (the start button) you called to initalizeAudioAnalyzer but that was already intialized with your useEffect on functionCont.jsx and doesn't need to be initialized again. Once I removed this, it all worked.
Here is the fixed up code that is now playing the audio:
functionCont.jsx:
import React, { useCallback, useEffect, useMemo, useState } from "react";
import VisualDemo from "./functionViz";
// import VisualDemo from "./VisualDemo";
import soundFile from "../audio/water.mp3";
const FunctionCont = () => {
const audioFile = useMemo(() => {
const audio = new Audio();
audio.crossOrigin = "anonymous";
return audio;
} , []);
const [audioData, setAudioData] = useState();
let frequencyBandArray = [...Array(25).keys()];
const initializeAudioAnalyser = useCallback(() => {
const audioContext = new AudioContext();
const source = audioContext.createMediaElementSource(audioFile);
const analyser = audioContext.createAnalyser();
audioFile.src = soundFile;
analyser.fftSize = 64;
source.connect(audioContext.destination);
source.connect(analyser);
setAudioData(analyser);
audioFile.play();
}, [audioFile]);
useEffect(() => {
initializeAudioAnalyser();
}, [initializeAudioAnalyser]);
const getFrequencyData = useCallback((styleAdjuster) => {
const bufferLength = audioData.frequencyBinCount;
const amplitudeArray = new Uint8Array(bufferLength);
audioData.getByteFrequencyData(amplitudeArray);
styleAdjuster(amplitudeArray);
}, [audioData]);
return (
<div>
<VisualDemo
frequencyBandArray={frequencyBandArray}
getFrequencyData={getFrequencyData}
// audioData={audioData}
audioFile={audioFile}
/>
</div>
);
};
export default FunctionCont;
functionViz.jsx
import React, { useRef, useEffect, useState } from "react";
import Paper from "#material-ui/core/Paper";
import IconButton from "#material-ui/core/IconButton";
import Tooltip from "#material-ui/core/Tooltip";
import EqualizerIcon from "#material-ui/icons/Equalizer";
import { makeStyles } from "#material-ui/core/styles";
import "../stylesheets/App.scss";
const useStyles = makeStyles((theme) => ({
flexContainer: {
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
paddingTop: "25%"
}
}));
const VisualDemo = (props) => {
const classes = useStyles();
const amplitudeValues = useRef(null);
function adjustFreqBandStyle(newAmplitudeData) {
amplitudeValues.current = newAmplitudeData;
let domElements = props.frequencyBandArray.map((num) =>
document.getElementById(num)
);
for (let i = 0; i < props.frequencyBandArray.length; i++) {
let num = props.frequencyBandArray[i];
domElements[
num
].style.backgroundColor = `rgb(0, 255, ${amplitudeValues.current[num]})`;
domElements[num].style.height = `${amplitudeValues.current[num]}px`;
}
}
function runSpectrum() {
props.getFrequencyData(adjustFreqBandStyle);
requestAnimationFrame(runSpectrum);
}
function handleStartButtonClick() {
requestAnimationFrame(runSpectrum);
}
return (
<div>
<div>
<Tooltip title="Start" aria-label="Start" placement="right">
<IconButton
id="startButton"
onClick={handleStartButtonClick}
// disabled={!!props.audioData ? true : false}
>
<EqualizerIcon />
</IconButton>
</Tooltip>
</div>
<div className={classes.flexContainer}>
{props.frequencyBandArray.map((num) => (
<Paper
className={"frequencyBands"}
elevation={4}
id={num}
key={num}
/>
))}
</div>
</div>
);
};
export default VisualDemo;

How to show an image when Chrome is offline

I'm trying to show an image when the Chrome browser is offline, and when it's online show the webpage.
I transferred the image to base64 data and tried to load it in the img tag, however the base64 data is too large.
Is there a way to show an image when the browser is offline?
import imageToBase64 from "image-to-base64";
const Home = () => {
const [isOnline, setIsOnline] = useState(true);
// Checks to see if the browser has internet connection or not
window.addEventListener("online", () => setIsOnline(true));
window.addEventListener("offline", () => setIsOnline(false));
//Link to the image
const idleImgUrl = `${window.location.href}${coffeeMachine}`;
//convert image to base64 and save to local storage
imageToBase64(idleImgUrl)
.then(res => {
window.localStorage.setItem("idleImgData", res);
})
.catch(err => {
console.log(err);
});
return (
isOnline
? (<div>The web page to show</div>)
:
// <p> tag shows
<p>The browser is offline now</p>
// img tag does not show
(<img src={window.localStorage.getItem("idleImgData"} />)
);
};
Any help would be appreciated...
The trick is to load the image while the user agent still has an internet connection. The image won't be downloaded until you render the <img> tag. The cached image can then be displayed without issue later.
I wrote a short create-react-app example to illustrate.
import React, { useState, useEffect, useCallback } from 'react';
const App = () => {
const [online, setOnline] = useState(true);
const onlineListener = useCallback(() => setOnline(true), [setOnline]);
const offlineListener = useCallback(() => setOnline(false), [setOnline]);
useEffect(() => {
window.addEventListener('online', onlineListener);
window.addEventListener('offline', offlineListener);
return () => {
window.removeEventListener('online', onlineListener);
window.removeEventListener('offline', offlineListener);
};
}, [onlineListener, offlineListener]);
return (
<div className="App">
<img
style={online ? { display: 'none' } : undefined}
src="TODO"
alt="no internet"
/>
</div>
);
};
export default App;
It displays an image when the user agent loses connection and hides it again when connection is restored. It obviously won't work if connection is cut to begin with, but in such a case how did the user load your application 🤔?
If you are offline, you might not even be able to load your react bundle.js file in the first place and there is nothing you can do about it.
Also, I don't see the advantage of keeping it in your localStorage in this case. Browsers are probably going to cache it anyway, if size matters here.
If the user was able to load your bundle, you can just store the b64 hardcoded as a variable directly in your bundle or initiate an ajax on componentDidMount (using useEffect since you use hooks).
const Home = () => {
const [base64Img, setBase64Img] = useState();
const [isOnline, setIsOnline] = useState(true);
const setOnline = () => setIsOnline(true);
const setOffline = () => setIsOnline(false);
useEffect(() => {
initiateB64Retrieval().then((b64) => setBase64Img(b64));
window.addEventListener('online', setOnline);
window.addEventListener('offline', setOffline);
return () => {
window.removeEventListener('online', setOnline);
window.removeEventListener('offline', setOffline);
}
}, [])
...
}
Always good practice to remove your event listeners. Note that you cannot remove event listeners if passed with anonymous functions or when using .bind() as it creates another reference for the function.

Categories

Resources