I want to create a component that will take any image and make it spin in circles.
I managed to do so but appears I have an issue with setting the interval cleanup function as it starts to switch quickly from one state to another and the picture spins like crazy.
This is the spinner component
import classes from './Spinner.module.css'
import { useState , useEffect} from 'react';
const Spinner = (props) =>{
const [Timer, setTimer] = useState('5');
useEffect(() => {
const interval = setInterval(() => {
let newT;
if(Timer==='5'){
newT='1';
}
else{
newT='5';
}
setTimer(newT);
}, 2000);
console.log(Timer);
return clearInterval(interval);
}, [Timer])
return <img style={{animation: `${classes.spin} ${Timer}s linear infinite`}} src={props.img} alt="img"/>
};
export default Spinner ;
Spin CSS :
#keyframes spin {
from {transform:rotate(0deg);}
to {transform:rotate(360deg);}
}
When the interval created cleanup function it did not work, since it was not in a function.
Change was in the return function of the useEffect component.
const Spinner = (props) => {
const [Timer, setTimer] = useState("5");
useEffect(() => {
const interval = setInterval(() => {
setTimer((prevState) => prevState ==='5' ? '1' : '5');
}, 2000);
console.log(Timer);
return ()=>{clearInterval(interval)};
}, [Timer]);
return (
<img
style={{ animation: `${classes.spin} ${Timer}s linear infinite` }}
src={props.img}
alt="img"
/>
);
};
Related
I wanted to calculate the user scroll height , so I created a custom hook. and I wanted to share this value to another component. but it doesnt work.
code:
const useScroll = () => {
let scrollHeight = useRef(0);
const scroll = () => {
scrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
};
useEffect(() => {
window.addEventListener("scroll", scroll);
return () => {
window.removeEventListener("scroll", () => {});
};
}, []);
return scrollHeight.current;
};
export default useScroll;
the value is not updating here.
but if I use useState here , it works. but that causes tremendous amount of component re-rendering. can you have any idea , how its happening?
Since the hook won't rerender you will only get the return value once. What you can do, is to create a useRef-const in the useScroll hook. The useScroll hook returns the reference of the useRef-const when the hook gets mounted. Because it's a reference you can write the changes in the useScroll hook to the useRef-const and read it's newest value in a component which implemented the hook. To reduce multiple event listeners you should implement the hook once in the parent component and pass the useRef-const reference to the child components. I made an example for you.
The hook:
import { useCallback, useEffect, useRef } from "react";
export const useScroll = () => {
const userScrollHeight = useRef(0);
const scroll = useCallback(() => {
userScrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
}, []);
useEffect(() => {
window.addEventListener("scroll", scroll);
return () => {
window.removeEventListener("scroll", scroll);
};
}, []);
return userScrollHeight;
};
The parent component:
import { SomeChild, SomeOtherChild } from "./SomeChildren";
import { useScroll } from "./ScrollHook";
const App = () => {
const userScrollHeight = useScroll();
return (
<div>
<SomeChild userScrollHeight={userScrollHeight} />
<SomeOtherChild userScrollHeight={userScrollHeight} />
</div>
);
};
export default App;
The child components:
export const SomeChild = ({ userScrollHeight }) => {
const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
console.log("userScrollHeight from SomeChild", userScrollHeight.current);
};
return (
<div style={{
width: "100vw",
height: "100vh",
backgroundColor: "aqua"
}}>
<h1>SomeChild 1</h1>
<button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
</div>
);
};
export const SomeOtherChild = ({ userScrollHeight }) => {
const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
console.log("userScrollHeight from SomeOtherChild", userScrollHeight.current);
};
return (
<div style={{
width: "100vw",
height: "100vh",
backgroundColor: "orange"
}}>
<h1>SomeOtherChild 1</h1>
<button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
</div>
);
};
import { useRef } from 'react';
import throttle from 'lodash.throttle';
/**
* Hook to return the throttled function
* #param fn function to throttl
* #param delay throttl delay
*/
const useThrottle = (fn, delay = 500) => {
// https://stackoverflow.com/a/64856090/11667949
const throttledFn = useRef(throttle(fn, delay)).current;
return throttledFn;
};
export default useThrottle;
then, in your custom hook:
const scroll = () => {
scrollHeight.current =
window.pageYOffset ||
(document.documentElement || document.body.parentNode || document.body)
.scrollTop;
};
const throttledScroll = useThrottle(scroll)
Also, I like to point out that you are not clearing your effect. You should be:
useEffect(() => {
window.addEventListener("scroll", throttledScroll);
return () => {
window.removeEventListener("scroll", throttledScroll); // remove Listener
};
}, [throttledScroll]); // this will never change, but it is good to add it here. (We've also cleaned up effect)
Code sandbox link: https://codesandbox.io/s/useinterval-customhook-iucj8q?file=/src/components/Displaytimer.js
I have created a custom hook for clock countdown while I am passing minutes input field values and seconds input fields as a prop to the child component it is taking the values too but when I click the start button it is still showing the 0. I think this is taking initial values I have used promises too and console logging each and every value but no use.
Image for output:
APP.js
import "./styles.css";
import Timer from "./components/Timer";
export default function App() {
return (
<div className="App">
<Timer />
</div>
);
}
Timer.js
import { useState, useRef } from "react";
import DisplayTimer from "./Displaytimer";
export default function Timer() {
const [min, setMins] = useState(0);
const [sec, setSecs] = useState(0);
const refValueMinutes = useRef();
const refValueSeconds = useRef();
const onchangeMinutes = (e) => {
// refValueMinutes.current = Number(e.target.value);
// const currVal = refValueMinutes.current;
setMins(Number(e.target.value));
};
const onchangeSeconds = (e) => {
// refValueSeconds.current = Number(e.target.value);
// const currVal = refValueSeconds.current;
setSecs(Number(e.target.value));
};
return (
<div>
<h2>Minutes: </h2> <br />
<input onChange={onchangeMinutes} />
<h2>Seconds: </h2> <br />
<input onChange={onchangeSeconds} />
<br />
<br />
<DisplayTimer min={min} sec={sec} />
</div>
);
}
enter image description here
UseInterval.js(custom hook)
import { useRef, useEffect } from "react";
export default function UseInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => {
clearInterval(id);
};
}
}, [delay]);
}
I thought this is due to DOM painting to the web page before the values get initiated to the state so I have tried using promises but no result and suggest me a good way to render this design to the webpage.
I have used use effect too:
useEffect(() => {
startTime();
stopTime();
resetTime();
}, []);
I am working on a React project in that I have a button, for that button I have written one onClick function now what I need is when I click the button it only needs to change background color only to mobile screen from min(0px) to max(576px) in this screen only the function change has to apply.
This is my code
import React, { useState } from 'react';
import './App.css';
function App() {
const [color,setColor]=useState('red');
const [textColor,setTextColor]=useState('white');
const changeBackGround =() =>{
{setColor("black");setTextColor('red')}
}
return (
<div className="App">
<button style={{background:color,color:textColor}} onClick={changeBackGround} className='btn btn-primary'>Click here</button>
</div>
);
}
export default App
If you have any questions please let me know. Thank you
Have a state object that updates the the className on the button click. Update the className in the css media query.
import React, { useState } from 'react';
import './App.css';
function App() {
const [color,setColor]=useState('red');
const [textColor,setTextColor]=useState('white');
const [buttonClassName, setButtonClassName] = useState("");
const changeBackGround = () =>{
setColor("black");
setTextColor('red');
setButtonClassName("btn-update");
}
return (
<div className="App">
<button
style={{background:color,color:textColor}}
onClick={changeBackGround}
className={`btn btn-primary ${buttonClassName}`}>
Click here
</button>
</div>
);
}
export default App
#media screen and (max-width: 576px) {
.btn-update {
background-color: "green";
}
}
You can do this in 2 ways
check your window.innerWidth . But this will not work when you resize your window in the browser. To Test this what you can do is resize your browser window so the width is less than 576px and refresh your screen and click the button now .
const changeBackGround =() =>{
if(window.innerWidth < 576){
setColor("black");
setTextColor('red')}
} else {
...do something
}
}
Attach an event listener which listens for your resize event , now when you resize the window the width is maintained in state.
function App() {
const [deviceSize, changeDeviceSize] = useState(window.innerWidth);
const [color, setColor] = useState('red');
const [textColor, setTextColor] = useState('white');
useEffect(() => {
const handleResize = () => changeDeviceSize(window.innerWidth);
window.addEventListener('resize', handleResize);
// don't forget to remove the event listener on unmounting the component
return () => window.removeEventListener('resize', handleResize);
}, []);
const changeBackGround = () => {
if (deviceSize < 576) {
{
setColor('black');
setTextColor('red');
}
}
};
return (
<div className="App">
<button
style={{background: color, color: textColor}}
onClick={changeBackGround}
className="btn btn-primary"
>
Click here
</button>
</div>
);
}
If you want to trigger things dynamically, use custom hooks to get window size (generic) and another custom hook to check if it's valid for mobile (can be kept in a separate hooks folder).
useWindowSize.js
// Hook from https://usehooks.com/useWindowSize/
function useWindowSize() {
// Initialize state with undefined width/height so server and client renders match
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined
});
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
}
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array ensures that effect is only run on mount
return windowSize;
}
useIsMobile.js
const MAX_SIZE_FOR = { mobile: 576 };
const useIsMobile = () => {
const { width } = useWindowSize();
return width < MAX_SIZE_FOR;
};
yourComponent.js
import React, { useState } from "react";
import { useIsMobile } from './useIsMobile'
import "./App.css";
function App() {
const [style, setStyle] = useState({ background: "red", textColor: "white" });
const isMobile = useIsMobile();
const changeBackGround = () => {
if (isMobile) {
setStyle({ ...style, background: "black", textColor: "red" });
}
};
return (
<div className="App">
<button style={style} onClick={changeBackGround} className="btn btn-primary">
Click here
</button>
</div>
);
}
export default App;
You can even change the color on the fly via useEffect
const { width } = useWindowSize();
useEffect(changeBackGround, [width]);
You can create a state variable to update when the screen gets to a certain width. In a useEffect(), you can add a eventListener to the window that listens to the screen resizing. When the screen gets resized to a certain width, we update the state and use it to do conditional rendering in the return.
const [show, setShow] = useState(false); // state value for showing / hiding
useEffect(() => {
const handleResize = () => {
window.innerWidth < 576 ? setShow(true) : setShow(false); // set hide / show
}
window.addEventListener("resize", handleResize); // add event listener
}, []);
return ({
show ? <h1>show</h1>: <h1>hide</h1>
});
I am trying to implement a navbar that has a blur effect when scrolling.
This works, but when I refresh the page, the scrollbar stays in the same position and I don't get any result from window.pageYOffset. The result of this is that I have a transparent navigation bar.
I'm also using TailwindCSS, but I think this doesn't matter.
Code example:
import React, { useState, useEffect } from 'react'
const Navigation: React.FC = () => {
const [top, setTop] = useState(true);
useEffect(() => {
const scrollHandler = () => {
window.pageYOffset > 20 ? setTop(false) : setTop(true)
};
window.addEventListener('scroll', scrollHandler);
return () => {
window.removeEventListener('scroll', scrollHandler);
}
}, [top]);
return (
<header className={`fixed w-full z-30 ${!top && 'bg-white dark:bg-black bg-opacity-80 dark:bg-opacity-80 backdrop-blur dark:backdrop-blur'}`}>
</header>
);
};
export default Navigation
You need to explicitly call scrollHandler() inside the useEffect if you want the navbar to keep its blurred state when the page is refreshed.
useEffect(() => {
const scrollHandler = () => {
setTop(window.pageYOffset <= 20)
};
window.addEventListener('scroll', scrollHandler);
// Explicit call so that the navbar gets blurred when component mounts
scrollHandler();
return () => {
window.removeEventListener('scroll', scrollHandler);
}
}, []);
You can also remove top from the useEffect's dependencies array, you only need it to run when the component is mounted.
I'm creating a timer with React hooks. It's a simple component that is used in a quiz. Each question has a defined duration so the timer should start at this duration and start decreasing one second at a time. My problem is that the component does what is supposed to do, but when I go to the next question the state doesn't initialize to the duration passed in the props but continues with the counter...
const Timer = ({ duration }) => {
const [counter, setCounter] = useState(duration);
const timer = useRef();
debugger;
console.log("counter: " + counter);
const setTimer = () => {
if (counter <= duration) {
setCounter(counter - 1);
}
};
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
timer.current = setTimeout(() => setTimer(), 1000);
return () => {
if (timer.current) {
console.log("ClearInterval in Timer");
clearTimeout(timer.current);
}
};
}, [counter]);
return (
<div>
<p>time left {counter} seconds</p>
</div>
);
};
export default Timer;
I'm rendering the Timer component from the Card component:
import React from "react";
import QuizButton from "../QuizButton/QuizButton";
import Timer from "../Timer/Timer";
import "./styles.css";
const QuizQuestion = ({ question, responses, checkAnswerFn, duration }) => (
<article className='card'>
<header>
<Timer duration={duration} />
</header>
<div>
<p>{question}</p>
</div>
<footer>
{responses.map((response, i) => {
return (
<QuizButton key={i} onClick={checkAnswerFn(response)}>
{response}
</QuizButton>
);
})}
</footer>
</article>
);
export default QuizQuestion;
Does anyone know why the state is not initialized to duration after the question re-renders?
Look a bit complex to drive you how and why your code is not working as expected, but here is a working example just did it today, hope it helps to point you in the right direction:
const [counter, setCounter] = useState(15000);
...
useEffect(() => {
const myInterval = () => {
if (counter > 1000) {
setCounter(state => state - 1000)
} else if (counter !== 0){
setCounter(0);
clearInterval(interval);
}
}
const interval = setInterval(myInterval, 1000);
return () => {
clearInterval(interval)
}
}, [counter]);
...
console.log(counter); // 15000, 14000, 13000, ...
The condition in the setTimer function is not allowing the counter to initialize. Let your function decrement the counter until and unless the counter value is greater than 0 otherwise initialize it with the duration. Replace your function with the following, it will help you initialize the timer back to the duration.
const setTimer = () => {
setCounter(counter > 0 ? counter - 1 : duration);
};
UPDATED 2 - Added Question Array and Question based timer data
UPDATED 1 - I THINK IT WORKS NOW AS YOUR WISH TO TO BE LIKE
Summary - I have lifted some states up.
So the edited answer is: Since we need to resend new prop data to the Child Timer component while the trigger was based on the child component which I have demonstrated using a click from the child Timer component, we passed a function from the parent Question component to the child component which would be called on an onClick listener via the prop. Therefore the function updates the parent's (Question.js) state re-rendering the child (Timer.js) and passing new prop values whichever we have set as new. Rather than relaying on timer based change we also changed to data based trigger like the Question Data changed to invoke the useEffect.
I hope the explanation works and and solved your use case.
..
//Question.js
import React, { useState, useEffect } from "react";
import Timer from "./Timer";
import "./styles.css";
const Question = () => {
const questionPool = [
{ number: 1, text: "lorem", timer: 6 },
{ number: 2, text: "lorem", timer: 10 },
{ number: 3, text: "lorem", timer: 20 },
{ number: 4, text: "lorem", timer: 3 },
{ number: 5, text: "lorem", timer: 12 }
];
const [countDown, setCountDown] = useState(questionPool[0].timer);
const [currentQuestion, setCurrentQuestion] = useState(
questionPool[0].number
);
const resetCount = (newQuestion) => {
newQuestion < questionPool.length + 1
? setCurrentQuestion(newQuestion)
: setCurrentQuestion(1);
};
useEffect(() => {
if (questionPool[currentQuestion]) {
setCountDown(questionPool[currentQuestion].timer);
} else {
setCountDown(questionPool[0].timer);
}
}, [currentQuestion]);
return (
<>
<Timer
duration={countDown}
resetCountDown={resetCount}
questionNumber={currentQuestion}
/>
</>
);
};
export default Question;
....
//Timer.js
import React, { useEffect, useState } from "react";
import "./styles.css";
const Timer = ({ duration, resetCountDown, questionNumber}) => {
const [counter, setCounter] = useState(duration);
let interval = null;
useEffect(() => {
const myInterval = () => {
if (counter > 1) {
setCounter((state) => state - 1);
} else if (counter !== 0) {
setCounter(0);
clearInterval(interval);
}
};
interval = setInterval(myInterval, 1000);
return () => {
clearInterval(interval);
};
}, [counter]);
useEffect(() => {
if (!interval) {
setCounter(duration);
}
}, [questionNumber]);
return (
<div>
<p>
time left {counter} seconds For Question: {questionNumber}
</p>
<button type="button" onClick={() => resetCountDown(questionNumber + 1)}>
next
</button>
</div>
);
};
export default Timer;
CodeSandbox Extending from #Adolfo Onrubia to match your use case
Check if this helps.
********* DEPRECATED BELOW *******************
If you still want to dynamically add timer component, I think you need to push in a whole react component in the prop and have the component which is pushing a new component re-render so the child will render itself and a new question with a new component can appear.
In short,
Your QuestionsArray[{OBJECT WITH QUESTION DETAILS}] is in your main app.
Your have a useState like :
const [currentQuestion, setCurrentQuestion] =
useState(QuestionArray[0]);
And whenever a next button is pushed. You do
setCurrentQeustion(QuestionArray[NewIndex]);
Thus your master component App Components' State re-renders on useEffect of currentQuestion and passes that to the Timer.js/child component causing it to rerender with new Timeout duration.
It may also be worthwhile to check on props.children so rather than just sending as a regular prop you pass as template like.
<Template>
{currentQuestion}
</Template>
Thanks to Fauzul and Adolfo, I finally solved my issue.
I pass the id of the question to the props of the timer, and using the useEffect hook, everytime the id changes the counter gets updated to the duration...
QuizQuestion.js
import React from "react";
import PropTypes from "prop-types";
import QuizButton from "../QuizButton/QuizButton";
import Timer from "../Timer/Timer";
import "./styles.css";
const QuizQuestion = ({ question, responses, checkAnswerFn, duration, id }) => (
<article className='card'>
<header>
<Timer duration={duration} id={id} />
</header>
<div>
<p>{question}</p>
</div>
<footer>
{responses.map((response, i) => {
return (
<QuizButton key={i} onClick={checkAnswerFn(response)}>
{response}
</QuizButton>
);
})}
</footer>
</article>
);
QuizQuestion.propTypes = {
question: PropTypes.string.isRequired,
responses: PropTypes.array.isRequired,
checkAnswerFn: PropTypes.func.isRequired,
};
export default QuizQuestion;
Timer.js
import React, { useState, useEffect, useRef } from "react";
import PropTypes from "prop-types";
const Timer = ({ duration, id }) => {
const [counter, setCounter] = useState(duration);
const timer = useRef();
const setTimer = () => {
if (counter > 0) {
setCounter(counter - 1);
}
};
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
timer.current = setTimeout(() => setTimer(), 1000);
return () => {
if (timer.current) {
clearTimeout(timer.current);
}
};
}, [counter]);
useEffect(() => {
setCounter(duration);
}, [id]);
return (
<div>
<p>time left {counter} seconds</p>
</div>
);
};
Timer.propTypes = {
duration: PropTypes.number.isRequired,
id: PropTypes.number.isRequired,
};
export default Timer;