Smooth scrolling down after component with pdf is displayed in reactjs - reactjs

I would need smooth scrolling down to component with generated pdf. But it looks like that first is scrolled to view and then
component is mounted. How can I set waiting to component is displayed and then scroll to view this component?
When I click to button, the pdf will start to generate. After generating this pdf, I would like scroll to view this PDFViewer.
My solution does not work. 'await' has no effect on the type of this expression. OR Object is possibly 'null'.
I use reactjs 17.0.2, react-pdf/renderer 3.0.1, typescript.
Code:
import { PDFViewer } from "#react-pdf/renderer";
export const Print: NextPage = () => {
const [pdfGenerationEnabled, setPdfGenerationEnabled] = useState<boolean>(false);
const generatePDFAndView = async () => {
setPdfGenerationEnabled(true);
await document.getElementById("generated-pdf-view")?.scrollIntoView({ behavior: "smooth" });
}
return (
<DownloadAndPrint>
<h2>Generating PDF</h2>
<p>
Some long text
Some long text
Some long text
.
.
.
</p>
<Button
className="btn-generate-pdf"
onClick={() => { generatePDFAndView() }}
>
Vygenerovať pdf súbory
</Button>
{pdfGenerationEnabled
?
<>
<div id="generated-pdf-view" className="viewer-styled">
<PDFViewer showToolbar={true} style={{ height: "45vh", width: "100%" }}>
<ComponentPrint
data={data}
/>
</PDFViewer>
</div>
</>
: ""
}
</DownloadAndPrint>
);
};
EDITED: After added delay, it's works. After 3 seconds smooth scrolled to the pdf viewer. But I don't know how many seconds it should waiting? Sometimes generated pdf has 3 pages, sometimes 300 pages. According to the data.
const delay = (n) => new Promise(r => setTimeout(r, n*1000));
const generatePDFAndView = async () => {
setPdfGenerationEnabled(true);
await delay(3.0); // waiting 3s
document.getElementById("generated-pdf-view")?.scrollIntoView({ behavior: "smooth" });
}

My solution with delay and calculation time to wait according to the data. Maybe somebody it helps.
import { PDFViewer } from "#react-pdf/renderer";
export const Print: NextPage = () => {
const [pdfGenerationEnabled, setPdfGenerationEnabled] = useState<boolean>(false);
const itemsPerPage = 5;
const calculateTimeToDisplayPdfViewer = () => {
const numberOfItems = data?.items?.length;
if (numberOfItems > 0 && itemsPerPage > 0) {
let time = (numberOfItems / itemsPerPage) / 5; // 1 page = 1/5s = 0.20s
time = parseFloat(time.toFixed(2));
console.log("Waiting " + time + "s.");
return time;
} else {
return 2.0;
}
}
const delay = (n) => new Promise( r => setTimeout(r, n*1000)); // waiting in seconds
const generatePDFAndView = async () => {
setPdfGenerationEnabled(true);
await delay(calculateTimeToDisplayPdfViewer());
document.getElementById("generated-pdf-view")?.scrollIntoView({ behavior: "smooth" });
}
return (
<DownloadAndPrint>
<h2>Generating PDF</h2>
<p>
Some long text
Some long text
Some long text
.
.
.
</p>
<Button
className="btn-generate-pdf"
onClick={() => { generatePDFAndView() }}
>
Vygenerovať pdf súbory
</Button>
{pdfGenerationEnabled
?
<>
<div id="generated-pdf-view" className="viewer-styled">
<PDFViewer showToolbar={true} style={{ height: "45vh", width: "100%" }}>
<ComponentPrint
data={data}
/>
</PDFViewer>
</div>
</>
: ""
}
</DownloadAndPrint>
);
};

Related

why the change of variant is not captured in framer motion?

I am trying to perform an animation fadein and fadeout to the images in a gallery with framer motions variants. The problem is I didn't manage to trigger the second animation.
So far, I have created a holder for the animation to be displayed till the image load, which I call DotLottieWrapper. I trigger the animation of this holder in the first render and when the loading state changes but the second looks like is not happening. My code is bellow:
const [loading,setLoading] = useState(1);
const loadedDone = () => {
const animationTime = 6000
async function wait() {
await new Promise(() => {
setTimeout(() => {
setLoading(0)
}, animationTime);
})
};
wait();
}
const StyledWrapper = wrapperStyle ?
styled(wrapperStyle)`position:relative;` :
styled(imageStyle)`
${cssImageStyle(src)}
`;
const animationVariants = {
0:{
backgroundColor:'#300080',
opacity:0,
transition:{duration:1.5}
},
1:{
opacity:1,
backgroundColor:'#702090',
transition:{duration:1.5}
}
}
return (
<StyledWrapper
as={NormalDiv}
onClick={handleClick || function () { }}
>
{(loading < 2) && (
<DotLottieWrapper
initial='0'
animate={loading===1?'1':'0'}
variants={animationVariants}
key={IDGenerator()}
>
</DotLottieWrapper>
)}
{(loading < 2) && (
<img
key={IDGenerator()}
onLoad={() => {
loadedDone();
}}
src={src}
/>
)}
</StyledWrapper>
)
Have I done something wrong?

unable to update name and value every 10 seconds

I am trying to implement react counter. every 10 seconds I need to update label and progress bar. But in label I could able to display 1 to 6 in 60 seconds successfully. but in timer due to some issue even though it reaches 60 seconds progress bar percange showing 80% only.
timer logic
const [number, setNumber] = useState(0);
const [progBarVal, setProgBarValr] = useState(0);
useEffect(() => {
if (number >= 6) {
return;
}
const intervalID = setTimeout(() => {
setNumber((t) => t + 1);
setProgBarValr((t) => t + 10);
}, 1000);
return () => clearInterval(intervalID);
}, [number, progBarVal]);
with in the return statement
return{
<div > {number}/6 </div>
<Progress done={progBarVal} />
}
progress bar logic
import React, { useState } from 'react';
import './progress.scss';
const Progress = ({ done }) => {
const [style, setStyle] = useState({});
setTimeout(() => {
const newStyle = {
opacity: 1,
width: `${done}%`,
};
setStyle(newStyle);
}, 200);
return (
<div className='met-prog__progress'>
<div className='met-prog__progress-done' style={style}>
{done}%
</div>
</div>
);
};
export default Progress;
I am trying to do if number is 1 the progBarVal 10 like that.
someone help me to understand where it went wrong.

Reset LinearProgress countdown by click on the button in ReactJs

Intro: In the part of my reactjs app, after entering mobile number, a pin code is send to the user and then a modal is open and user should enter pin code. In this modal there is a countdown that render a progress-bar for waiting time.
Problem: After complete progress (For example, the user has not entered a pin or received a message), resend button is appears and if user click on, this progress should be reset.
Question: How to reset progress-bar by clicking on the button? Or How to reset component with initial states?
Code: CounterProgress
import React, { useEffect, useState } from 'react'
import Countdown from "react-countdown";
import { Button, LinearProgress,} from "#material-ui/core";
function CounterProgress() {
const waitingTime = 10000;
const [counterRemained, setCounterRemained] = useState(waitingTime);
const [currentTime, setCurrentTime] = useState(Date.now() + counterRemained);
const [resend, setResend] = useState(true);
const counterRenderer = ({ minutes, seconds, completed, total }) => {
setCounterRemained(total)
const mainMin = waitingTime;
const onePercent = mainMin / 100;
const leftedPercent = total / onePercent;
return (
resend ? (
<>
<LinearProgress
variant="determinate"
value={leftedPercent}
dir="ltr"
style={{ height: "0.7rem", borderRadius: "10px" }}
className={` w-75 mx-auto`}
/>
<div style={{
display: "block",
marginLeft: "auto",
marginRight: "auto",
}}>
<Button
disabled={true}
style={counterRemained === 0 ? { textColor: "#6dd06d" } : {}}
>
resend code
</Button>
<span style={{ textColor: "black", fontSize: "10px" }}>{minutes}:{seconds}</span>
</div>
</>
) : (
<>
<Button
style={counterRemained === 0 ? { textColor: "#6dd06d" } : {}}
onClick={() => setResend(true)}
>
resend code
</Button>
</>
)
);
};
return (
<Countdown
date={currentTime}
renderer={counterRenderer}
onComplete={() => {
setResend(false);
}}
/>
);
}
export default CounterProgress;
This code work when user click button but progress-bar is not reset and does not work.
My solutions but do not works:
Reset CounterProgress component using force update.
Using key option inside countdown. This method is working automatically and without onClick.
Using key option:
const [times, setTimes] = useState([Date.now() + waitingTime, Date.now() + 2 * waitingTime]);
const [currentTimeIndex, setCurrentTimeIndex] = useState(0);
return (
<Countdown
date={currentTime}
key={currentTimeIndex}
renderer={counterRenderer}
onComplete={() => {
console.log("completed", currentTimeIndex)
if (times.length - 1 <= times.indexOf(currentTime)) return;
setCurrentTimeIndex(currentTimeIndex + 1);
setCurrentTime(new Date(times[currentTimeIndex + 1]));
}}
/>
);
Well, you can use useEffect hook for resending the code when the progress bar finishes.
You can put resend as a dependency in useEffect so whenever the value of resend updates or changes useEffect will get called.
Example:
useEffect(() => {
//reset your state to initial state.
}, [resend])
I used key option for countdown. I made resend state as a state three values (-1, 0, 1).
The part of code that changed:
const waitingTime = 120000;
const [counterRemained, setCounterRemained] = useState(waitingTime);
const [times, setTimes] = useState([Date.now() + waitingTime, Date.now() + 2 * waitingTime]);
const [currentTime, setCurrentTime] = useState(Date.now() + counterRemained);
const [currentTimeIndex, setCurrentTimeIndex] = useState(0);
const [resend, setResend] = useState(-1);
useEffect(() => {
// resend sms to user
if (resend === 0) { // by clicking on the button, resend be 0
setResend(-1) // for initializing
setCurrentTimeIndex(currentTimeIndex + 1);
setCurrentTime(new Date(times[currentTimeIndex + 1]));
}
}, [resend])
return (
<>
<Countdown
date={currentTime}
key={currentTimeIndex}
renderer={counterRenderer}
onComplete={() => {
if (times.length - 1 <= times.indexOf(currentTime)) return;
}}
/>
</>
);

Why my setInterval() is speeding up inside my React image slider?

Why my interval is speeding up?
When I press any of my buttons NextImage() or PrevImage() my interval starts speeding up and the image starts glitching. Any advice or help?
Here's my code =>
//Image is displayed
const [image, setImage] = React.useState(1);
let imageShowed;
if (image === 1) {
imageShowed = image1;
} else if (image === 2) {
imageShowed = image2;
} else if (image === 3) {
imageShowed = image3;
} else {
imageShowed = image4;
}
// Auto change slide interval
let interval = setInterval(
() => (image === 4 ? setImage(1) : setImage(image + 1)),
5000
);
setTimeout(() => {
clearInterval(interval);
}, 5000);
// Change image functionality
const ChangeImage = (index) => {
setImage(index);
};
/ /Next image
const NextImage = () => {
image === 4 ? setImage(1) : setImage(image + 1);
};
// Previous image
const PrevImage = () => {
image === 1 ? setImage(4) : setImage(image - 1);
};
When you need to have some logic which is depend on changing a variable, it's better to keep those logic inside useEffect
const interval = useRef(null);
const timeout = useRef(null);
useEffect(() => {
interval.current = setInterval(
() => (image === 4 ? setImage(1) : setImage((i) => i + 1)),
5000
);
timeout.current = setTimeout(() => {
clearInterval(interval.current);
}, 5000);
return () => {
clearInterval(interval.current);
clearTimeout(timeout.current);
}
}, [image]);
one point to remember is that if you use a variable instead of using useRef it can increase the possibility of clearing the wrong instance of interval or timeout during the rerenders. useRef can keep the instance and avoid any unwanted bugs
Your approach causes so many problems and you should learn more about react (watch youtube tutorials about react), I did make a working example slider hope to help you and people in the future:
let interval;
const images = [
"https://picsum.photos/300/200?random=1",
"https://picsum.photos/300/200?random=2",
"https://picsum.photos/300/200?random=3",
"https://picsum.photos/300/200?random=4",
"https://picsum.photos/300/200?random=5",
];
const App = () => {
const [slide, setSlide] = React.useState(0);
React.useEffect(() => {
interval = setInterval(() => {
NextSlide();
clearInterval(interval);
}, 5000);
return () => {
clearInterval(interval);
};
}, [slide]);
const ChangeSlideDots = (index) => {
setSlide(index);
};
const NextSlide = () =>
setSlide((prev) => (slide === images.length - 1 ? 0 : prev + 1));
const PrevSlide = () =>
setSlide((prev) => (slide === 0 ? images.length - 1 : prev - 1));
return (
<div style={styles.root}>
<img style={styles.imageDiv} src={images[slide]} />
<button style={styles.buttons} onClick={PrevSlide}>
◁
</button>
<div style={styles.dotDiv}>
{images.map((_, i) => (
<div
key={i}
style={i === slide ? styles.redDot : styles.blackDot}
onClick={() => ChangeSlideDots(i)}
>
.
</div>
))}
</div>
<button style={styles.buttons} onClick={NextSlide}>
▷
</button>
</div>
);
}
const styles = {
root: {
display: "flex",
position: "relative",
width: 300,
height: 200,
},
buttons: {
backgroundColor: "rgb(255 255 255 / 37%)",
border: "none",
zIndex: 2,
flex: 1,
},
imageDiv: {
position: "absolute",
zIndex: 1,
width: 300,
height: 200,
},
dotDiv: {
flex: 10,
zIndex: 2,
fontSize: "30px",
display: "flex",
justifyContent: "center",
},
redDot: {
cursor: "pointer",
color: "red",
},
blackDot: {
cursor: "pointer",
color: "black",
},
};
ReactDOM.render(<App />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Anytime that you rerender your component, you will run the whole function once. So you will set an interval every time you use setImage(). In order to prevent this, you have to use side effect functions. here you should use useEffect() because you have a functional component. in order to make useEffect() only run once, you have to pass an empty array for dependecy array; So your useEffect will act like componentDidMount() in class components. try the code below:
let interval = null
useEffect(() => {
interval = setInterval(
() => (image === 4 ? setImage(1) : setImage(image + 1)),
5000
)
setTimeout(() => {
clearInterval(interval);
}, 5000)
}, [])
Thanks, everybody for your great answers appreciated a lot your time and help!
So, my final solution looks like this:
const images = [image1, image2, image3, image4];
const quotes = [
'Jobs fill your pockets, adventures fill your soul',
'Travel is the only thing you buy that makes you richer',
'Work, Travel, Save, Repeat',
'Once a year, go someplace you’ve never been before',
];
const App = () => {
//Image is displayed
const [image, setImage] = React.useState(0);
// Auto change slide interval
useEffect(() => {
return () => {
clearInterval(
setInterval((interval) => {
image === 3 ? setImage(1) : setImage(image + 1);
clearInterval(interval.current);
}, 5000)
);
};
}, [image]);
// Change image functionality
const ChangeImage = (index) => {
setImage(index);
};
//Next image
const NextImage = () => {
image === 3 ? setImage(1) : setImage(image + 1);
};
// Previous image
const PrevImage = () => {
image === 1 ? setImage(3) : setImage(image - 1);
};
return (
<Section>
<div className='slideshow-container'>
<div>
<img className='slider_image' src={images[image]} alt='slider' />
<h1 className='slider_title'>{quotes[image]}</h1>
</div>
<button className='slider_prev' onClick={PrevImage}>
❮
</button>
<button className='slider_next' onClick={NextImage}>
❯
</button>
</div>
<div>
<div>
{images.map((image, i) => (
<img
key={i}
alt={`slider${i}`}
src={image}
className='bottom_image'
onClick={() => ChangeImage(i)}
></img>
))}
</div>
</div>
</Section>
);
};

useState not firing after page is navigated to

The first time the Test component is navigated to, everything works correctly. React Navigation 5 is used to navigate to the Results page, and then navigate back to the Test component. However, although I receive the timerIsRunning prop correctly via the route.params this is not set and passed to the <Timer> as I would expect.
Have I misunderstood how this is meant to work?
Test component (abridged)
export const Test = ({ navigation, route }) => {
const { timed, category, minutes, timerIsRunning } = route.params; // data is received correctly
const [timerRunning, setTimerRunning] = React.useState(timerIsRunning); // this works the first time it's navigated to
return (
<View style={{ flex: 1, margin: 10 }}>
<ScrollView>
{timed ? <Timer seconds={minutes*60} timerRunning={timerRunning} /> : null} // timer component called here
</ScrollView>
<Button
onPress={() => {
setResponseDetails({ answered: false, index: null });
setActiveIndex(0);
setStoredResponses([]);
setTimerRunning(false); // this correctly stops the timer
navigation.navigate('Results', { // and navigates to the Results page
data: storedResponses,
category: category,
timed: timed,
minutes: minutes,
});
}}
disabled={!responseDetails.answered}
style={styles.button}
>
<Text>Results</Text>
<Icon name="arrow-dropright" />
</Button>
)
</View>
);
};
Results page (abridged)
export const Results = ({ navigation, route }) => {
const { category, data: testData, timed, minutes } = route.params;
return (
<View>
<Button
onPress={() =>
navigation.navigate('Test', { // Navigates back to test
timed: timed,
minutes: minutes,
category: category,
timerIsRunning: true // Correctly passes this prop, and is received in the Test component
})
}
>
<Icon name="arrow-back" />
<Text>Try the {category} test again</Text>
</Button>
</View>
);
};
Have you tried adding timerActive as one of your dependencies on the second useEffect?
React.useEffect(() => {
if (timeLimit > 0 && timerActive) {
setTimeout(() => {
// console.log('startTime, ', timeLimit);
settimeLimit(timeLimit - 1);
}, 1000);
}
if (timeLimit === 0 && timerRunning || !timerRunning) {
console.log('done');
}
}, [timeLimit, timerActive]);
Here is an example of a working timer that takes in seconds and timerRunning as props, please let me know if you need any help implementing it. You can update your question or add a working snippet.
const pad = (num) => `0${String(num)}`.slice(-2);
//moved formatTime out of the component
const formatTime = (timeLimit) => {
let displaySeconds = timeLimit % 60;
let minutes = Math.floor(timeLimit / 60);
//clean up formatting number
return `${pad(minutes)}:${pad(displaySeconds)}`;
};
const Timer = ({ seconds, timerRunning }) => {
const [timeLimit, settimeLimit] = React.useState(seconds);
//removed timerActive as it's a copy of timerRunning
React.useEffect(
() => {
if (timerRunning) {
//changed this to a interval and cancel it
// when timeLimit or timerRunning changes
let interval;
if (timerRunning) {
interval = setInterval(() => {
//using callback to get current limit value
// prevent using stale closure or needless
// dependency
settimeLimit((timeLimit) => {
//do the check here so timeLimit is not
// a dependency of this effect
if (timeLimit !== 0) {
return timeLimit - 1;
}
//do not keep running the interval
// if timeLimit is 0
clearInterval(interval);
return timeLimit;
});
}, 1000);
}
//clean up function will end interval
return () => clearInterval(interval);
}
},
//re run if seconds or timerRunning changes
[seconds, timerRunning]
);
//set local timeLimit again when seconds change
React.useEffect(() => settimeLimit(seconds), [seconds]);
return <div> {formatTime(timeLimit)}</div>;
};
function App() {
const [seconds, setSeconds] = React.useState(10);
const [running, setRunning] = React.useState(false);
return (
<div>
<div>
<label>
run timer
<input
type="checkbox"
checked={running}
onChange={() => setRunning((r) => !r)}
/>
</label>
<div>
<label>
<select
onChange={(e) =>
setSeconds(Number(e.target.value))
}
value={seconds}
>
<option value={1}>1</option>
<option value={10}>10</option>
<option value={100}>100</option>
<option value={1000}>1000</option>
</select>
</label>
</div>
</div>
<Timer seconds={seconds} timerRunning={running} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Okay, actually I worked this out - with React Navigation 5.x you can use the route.params in the useEffect to trigger an action. I ended up with this:
React.useEffect(() => {
setTimerRunning(timerIsRunning);
}, [route.params]);

Resources