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;
}}
/>
</>
);
Related
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>
);
};
I have a Navigation component in which the Menu Items float in separately on load and float out on click.
When I added Router and changed the items to Links, the exit animation didn't work because it loaded the new Route component right away.
I want to keep the items individual animation with Link functionality.
Here is the link:
https://codesandbox.io/s/elastic-leaf-fxsswo?file=/src/components/Navigation.js
Code:
export const Navigation = () => {
const navRef = useRef(null);
const onResize = () => {
setIsColumn(window.innerWidth <= 715);
};
const [clickOnMenu, setClick] = useState(false);
const [itemtransition, setTransition] = useState(
Array(menuItems.length).fill(0)
);
const [isColumn, setIsColumn] = useState(window.innerWidth <= 715);
const click = (e) => {
const copy = [...itemtransition];
const index = e.target.id;
setTransition(copy.map((e, i) => (Math.abs(index - i) + 1) / 10));
setTimeout(() => setClick(true), 50);
};
useEffect(() => {
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
return (
<AnimatePresence exitBeforeEnter>
{!clickOnMenu && (
<Nav ref={navRef}>
{menuItems.map((e, i) => {
const text = Object.keys(e)[0];
const value = Object.values(e)[0];
return (
<Item
id={i}
key={value}
animate={{
x: 0,
y: 0,
opacity: 1,
transition: { delay: (i + 1) / 10 }
}}
initial={{
x: isColumn ? 1000 : 0,
y: isColumn ? 0 : 1000,
opacity: 0
}}
exit={{
x: isColumn ? -1000 : 0,
y: isColumn ? 0 : -1000,
opacity: 0,
transition: { delay: itemtransition[i] }
}}
onClick={click}
>
{/*<Link to={`/${value}`}>{text}</Link>*/}
{text}
</Item>
);
})}
</Nav>
)}
</AnimatePresence>
);
};
In the sandbox in Navigation.js 69-70. row:
This is the desired animation.
69. {/*<Link to={`/${value}`}>{text}</Link>*/}
70. {text}
But when I use Link there is no exit animation
69. <Link to={`/${value}`}>{text}</Link>
70. {/*text*/}
Is there a workaround or I should forget router-dom.
Thank you in forward!
This may be a bit hackish, but with routing and transitions sometimes that is the nature. I suggest rendering the Link so the semantic HTML is correct and add an onClick handler to prevent the default navigation action from occurring. This allows any transitions/animations to go through. Then update the click handler of the Item component to consume the link target and issue an imperative navigation action on a timeout to allow transitions/animations to complete.
I used a 750ms timeout but you may need to tune this value to better suit your needs.
Example:
...
import { Link, useNavigate } from "react-router-dom";
...
export const Navigation = () => {
const navRef = useRef(null);
const navigate = useNavigate(); // <-- access navigate function
...
const click = target => (e) => { // <-- consume target
const copy = [...itemtransition];
const index = e.target.id;
setTransition(copy.map((e, i) => (Math.abs(index - i) + 1) / 10));
setTimeout(() => {
setClick(true);
}, 50);
setTimeout(() => {
navigate(target); // <-- navigate after some delta
}, 750);
};
...
return (
<AnimatePresence exitBeforeEnter>
{!clickOnMenu && (
<Nav ref={navRef}>
{menuItems.map((e, i) => {
const text = Object.keys(e)[0];
const value = Object.values(e)[0];
return (
<Item
...
onClick={click(`/${value}`)} // <-- pass target to handler
>
<Link
to={`/${value}`}
onClick={e => e.preventDefault()} // <-- prevent link click
>
{text}
</Link>
</Item>
);
})}
</Nav>
)}
</AnimatePresence>
);
};
...
I've came across this, which helped me this as far as I've got. Though, I'm trying to hack together a simple status component that instead of a checkbox, is just a div with styling to make it appear as a tiny dot that toggles between three strings, offline, wip and online onClick! Changing just the color upon change of state. (Practically speaking, I'll set an array of objects as offline and if toggled differently I'll store that preference.)
I'm just stuck trying to move away from a checkbox, I'll show you what I mean:
const STATUS_STATES = {
Online: "online",
Wip: "wip",
Offline: "offline",
};
function SomePage() {
const [status, setStatus] = useState(STATUS_STATES.Offline);
const handleChange = () => {
let updatedChecked;
if (status === STATUS_STATES.Online) {
updatedChecked = STATUS_STATES.Offline;
} else if (status === STATUS_STATES.Offline) {
updatedChecked = STATUS_STATES.Wip;
} else if (status === STATUS_STATES.Wip) {
updatedChecked = STATUS_STATES.Online;
}
setStatus(updatedChecked);
};
const Status = ({ value, onChange }) => {
const checkboxRef = React.useRef();
useEffect(() => {
if (value === STATUS_STATES.Online) {
console.log("online")
checkboxRef.current.checked = true;
checkboxRef.current.indeterminate = false;
} else if (value === STATUS_STATES.Offline) {
console.log("offline")
checkboxRef.current.checked = false;
checkboxRef.current.indeterminate = false;
} else if (value === STATUS_STATES.Wip) {
console.log("wip")
checkboxRef.current.checked = false;
checkboxRef.current.indeterminate = true;
}
}, [value]);
return (
<label>
<input ref={checkboxRef} type="checkbox" onChange={onChange} />
{/* I need to replace the line above with the line below */}
{/* while escaping the label element it's wrapped within */}
<div
className={
(value === "offline" && "offline") ||
(value === "wip" && "wip") ||
(value === "online" && "online")
}
onChange={onChange}
/>
</label>
);
};
return (
<>
<Status value={status} onChange={handleChange} />
<p>Is checked? {status}</p>
</>
)
}
.status{
width: 6px;
height: 6px;
background: #ee4f4f;
border-radius: 50%;
}
Any advice to approach this more efficiently?
This tidies it up quite a bit
codebox: https://codesandbox.io/s/intelligent-gareth-7qvko?file=/src/App.js:0-1050
import "./styles.css";
import React, { useState, useEffect } from "react";
const STATUS_STATES = ["Online", "Wip", "Offline"];
const STATUS_COLORS = ["green", "orange", "red"];
const Dot = ({ color, onClick }) => (
<div
onClick={onClick}
style={{ borderRadius: 5, height: 10, width: 10, background: color }}
/>
);
const Main = () => {
const [status, setStatus] = useState(0);
const handleClick = () => {
const newStatus = (status + 1) % STATUS_STATES.length;
setStatus(newStatus);
};
const text = STATUS_STATES[status];
return (
<>
<Dot onClick={handleClick} color={STATUS_COLORS[status]} />
<div onClick={handleClick}>{text}</div>
</>
);
};
export default Main;
You will see to loop through the statuses, I have put them through in an array, and used the remainder operator to get the next index.
The useEffect logic only needed a simple if for each value which has been written in short hand which (I think) is more readable for variables.
You will also notice the onclick on the checkbox input is wrapped in a timeout with a 0ms wait. This is a workaround because I couldnt prevent the default checkbox behaviour from happening. This ensures the click handling is run after the native logic.
I also reduced your array to just an array of strings - This was just to simplify the example.
I am working of a Guessing Game for 'React Native' where the user enters a number and the phone tries to guess it. Each time the phone generates a guess the user can click Greater/Lower. When the user entered number and the computer made guess equal each other we are taken to the game over screen.
The game over screen is not rendering. The logic to render the game over screen is placed inside of a useEffect()
Problem
useEffect is only fired once during the mounting phase and never again?
const { userSelectedNumber, onGameOver } = props;
useEffect(() => {
console.log(currentGuess, userSelectedNumber);
if (currentGuess === userSelectedNumber) {
onGameOver(rounds);
}
}, [userSelectedNumber, onGameOver]);*emphasized text*
(./screens/GameScreen.js)
We should exit the GameScreen when currentGuess === userSelectedNumber but this code is only run once.
Full code for GameScreen below:
import React, { useState, useRef, useEffect } from "react";
import { View, StyleSheet, Button, Text, Alert } from "react-native";
import NumberContainer from "../components/NumberContainer";
import Card from "../components/Card";
const randNumberGeneratorBetween = (min, max, exclude) => {
min = Math.ceil(min);
max = Math.floor(max);
const randNum = Math.floor(Math.random() * (max - min)) + min;
if (randNum === exclude) {
return randNumberGeneratorBetween(1, 100, exclude);
} else {
return randNum;
}
};
const GameScreen = props => {
const [currentGuess, setCurrentGuess] = useState(
randNumberGeneratorBetween(1, 100, props.userSelectedNumber)
);
const [rounds, setRounds] = useState(0);
const currentLow = useRef(1);
const currentHigh = useRef(100);
const { userSelectedNumber, onGameOver } = props;
useEffect(() => {
console.log(currentGuess, userSelectedNumber);
if (currentGuess === userSelectedNumber) {
onGameOver(rounds);
}
}, [userSelectedNumber, onGameOver]);
const nextGuessHandler = direction => {
if (
(direction === "lower" && currentGuess < props.userSelectedNumber) ||
(direction === "greater" && currentGuess > props.userSelectedNumber)
) {
Alert.alert("Don't Lie", "You know this is wrong", [
{ text: "Sorry", style: "cancel" }
]);
}
if (direction === "lower") {
currentHigh.current = currentGuess;
} else {
currentLow.current = currentGuess;
}
const nextNumber = randNumberGeneratorBetween(
currentLow.current,
currentHigh.current,
currentGuess
);
console.log('nextNumber',nextNumber);
setCurrentGuess(nextNumber);
setRounds(currRounds => currRounds + 1);
console.log('currRound',rounds);
};
return (
<View style={styles.screen}>
<Text>Opponents Guess</Text>
<NumberContainer>{currentGuess}</NumberContainer>
<Card style={styles.buttonContainer}>
<Button
title="Lower"
onPress={nextGuessHandler.bind(this, "lower")}
></Button>
<Button
title="Greater"
onPress={nextGuessHandler.bind(this, "greater")}
></Button>
</Card>
</View>
);
};
const styles = StyleSheet.create({
screen: {
flex: 1,
padding: 10,
alignItems: "center"
},
buttonContainer: {
flexDirection: "row",
justifyContent: "space-between",
marginTop: 20,
width: 300,
maxWidth: "80%"
}
});
export default GameScreen;
Project can be found here:
https://codesandbox.io/s/github/SMasood1/guessingGame?file=/screens/GameScreen.js:852-1039
You need to add rounds and currentGuess to the dependencies array in the useEffect hook
useEffect(() => {
console.log(currentGuess, userSelectedNumber);
if (currentGuess === userSelectedNumber) {
onGameOver(rounds);
}
}, [userSelectedNumber, onGameOver,currentGuess,rounds]);
Also it is considered a anti-pattern to use props to initialize a state, so I would recommend to add an other useEffect hook:
useEffect(()=>{
setCurrentGuess(randNumberGeneratorBetween(1, 100, props.userSelectedNumber))
},[props.userSelectedNumber]);
The useEffect hook causes the component to update whenever any of the values of the dependency array changes. Make sure the values you use to trigger that hook are in fact changing.
You can elegantly trigger useEffect by supplying a timestamp on you navigation.navigate call
e.g.
// someComponent.tsx
navigation.navigate('Home', {
showSubscriptionModal: true
})
// HomeScreen.tsx
const showSubscriptionModal = props.route.params?.showSubscriptionModal ?? false
useEffect(() => {
if(showSubscriptionModal) setIsShowingModal(true)
},[showSubscriptionModal])
will only fire once, while
// someComponent.tsx
navigation.navigate('Home', {
showSubscriptionModal: true,
updateTs: new Date()
})
// HomeScreen.tsx
const showSubscriptionModal = props.route.params?.showSubscriptionModal ?? false
useEffect(() => {
if(props.route.params?.showSubscriptionModal) setIsShowingModal(true)
},[showSubscriptionModal, props.route.params?.updateTs])
will fire every time you re-navigate to your screen via navigation.navigate()
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]);