I have a function that sets a reminder to pop up on the screen, but the message wont go away. Am I using the clearInterval with react hooks correctly in this function?
useEffect(() => {
const interval = setInterval(() => {
handleReminder(activeReminders);
}, 1000);
return () => clearInterval(interval);
}, [activeReminders]);
useEffect(() => {
const notesWithReminder = getNotesWithReminder(notes);
if (notesWithReminder.length > 0) {
setActiveReminders(notesWithReminder);
}
}, [notes]);
function getNotesWithReminder(notes) {
return notes.filter((note) => note.reminder && !note.isReminderShow);
}
function handleReminder(reminders) {
const activeRem = reminders.find((rem) => {
const now = Date.now();
const getRemTime = new Date(rem.reminder).getTime();
return getRemTime <= now;
});
setActiveReminder(activeRem);
setShowNotifyModal(true);
}
Message was not dismissing due to if statement which created a memory leak.
solution:
useEffect(() => {
const notesWithReminder = getNotesWithReminder(notes);
setActiveReminders(notesWithReminder);
}, [notes]);
Related
I'm trying to achieve smooth animation of searchbar (MUI Autocomplete). This should work only on Smartphones (Screen < 600px).
Here is an example (it is very buggy and open it on smartphone to see the animation): https://react-zxuspr-gjq5w8.stackblitz.io/
And here is my implementation, but I've a few problems with that:
The interval does not reset on dropdown close.
The React.useEffect() dependency is set to searchActive, which is changed dynamically.
I tried calling the callback function of React.useState(), but since the component is not destroyed, I am not sure if it makes sense.
The width of dropdown, which is also changed in the setInterval() function, is not smooth at all.
https://stackblitz.com/edit/react-zxuspr-gjq5w8?file=demo.js
Here is part of the component where the logic is implemented:
export function PrimarySearchAppBar() {
const [searchActive, setSearchActive] = React.useState(null);
const [acPaperWidth, setAcPaperWidth] = React.useState(null);
const [acPaperTransX, setAcPaperTransX] = React.useState(0);
const AcRef = React.useRef(null);
const isMobile = useMediaQuery(useTheme().breakpoints.down('sm'));
const options = top100Films.map((option) => {
const group = option.group.toUpperCase();
return {
firstLetter: group,
...option,
};
});
React.useLayoutEffect(() => {
if (AcRef.current) {
setAcPaperWidth(AcRef.current.offsetWidth);
}
console.log(acPaperWidth);
}, [AcRef]);
let interval;
React.useEffect(() => {
if (searchActive) {
if (acPaperTransX <= 39) {
interval = setInterval(() => {
setAcPaperWidth(AcRef.current.offsetWidth);
setAcPaperWidth((acPaperTransX) => acPaperTransX + 1);
if (acPaperTransX >= 38) {
clearInterval(interval);
}
console.log(acPaperTransX);
}, 10);
}
} else {
setAcPaperTransX(0);
clearInterval(interval);
}
}, [searchActive]);
return (
<>Hello World</>
);
}
The problem was that the interval variable was defined outside the React.useEffect() hook, so its value was not preserved on re-renders.
I was able to fix that by using React.useRef():
const intervalRef = React.useRef();
const acPaperTransXRef = React.useRef(acPaperTransX);
React.useEffect(() => {
if (searchActive) {
if (acPaperTransXRef.current <= 39) {
intervalRef.current = setInterval(() => {
setAcPaperWidth(AcRef.current.offsetWidth);
acPaperTransXRef.current += 0.1;
if (acPaperTransXRef.current >= 38) {
clearInterval(intervalRef.current);
acPaperTransXRef.current = 0;
}
console.log(acPaperTransXRef.current);
}, 2);
}
} else {
setAcPaperTransX(0);
acPaperTransXRef.current = 0;
clearInterval(intervalRef.current);
}
return () => clearInterval(intervalRef.current);
}, [searchActive]);
What I am trying to do is to update the reset the countdown after changing the status.
There are three status that i am fetching from API .. future, live and expired
If API is returning future with a timestamp, this timestamp is the start_time of the auction, but if the status is live then the timestamp is the end_time of the auction.
So in the following code I am calling api in useEffect to fetch initial data pass to the Countdown and it works, but on 1st complete in handleRenderer i am checking its status and updating the auctionStatus while useEffect is checking the updates to recall API for new timestamp .. so far its working and 2nd timestamp showed up but it is stopped ... means not counting down time for 2nd time.
import React, { useEffect } from 'react';
import { atom, useAtom } from 'jotai';
import { startTimeAtom, auctionStatusAtom } from '../../atoms';
import { toLocalDateTime } from '../../utility';
import Countdown from 'react-countdown';
import { getCurrentAuctionStatus } from '../../services/api';
async function getAuctionStatus() {
let response = await getCurrentAuctionStatus(WpaReactUi.auction_id);
return await response.payload();
}
const Counter = () => {
// component states
const [startTime, setStartTime] = useAtom(startTimeAtom);
const [auctionStatus, setAuctionStatus] = useAtom(auctionStatusAtom);
useEffect(() => {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
setStartTime(toLocalDateTime(response.end_time, WpaReactUi.time_zone));
});
}, [auctionStatus]);
//
const handleRenderer = ({ completed, formatted }) => {
if (completed) {
console.log("auction status now is:", auctionStatus);
setTimeout(() => {
if (auctionStatus === 'future') {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
});
}
}, 2000)
}
return Object.keys(formatted).map((key) => {
return (
<div key={`${key}`} className={`countDown bordered ${key}-box`}>
<span className={`num item ${key}`}>{formatted[key]}</span>
<span>{key}</span>
</div>
);
});
};
console.log('starttime now:', startTime);
return (
startTime && (
<div className="bidAuctionCounterContainer">
<div className="bidAuctionCounterInner">
<Countdown
key={auctionStatus}
autoStart={true}
id="bidAuctioncounter"
date={startTime}
intervalDelay={0}
precision={3}
renderer={handleRenderer}
/>
</div>
</div>
)
);
};
export default Counter;
You use auctionStatus as a dependency for useEffect.
And when response.status is the same, the auctionStatus doesn't change, so your useEffect won't be called again.
For answering your comment on how to resolve the issue..
I am not sure of your logic but I'll explain by this simple example.
export function App() {
// set state to 'live' by default
const [auctionStatus, setAuctionStatus] = React.useState("live")
React.useEffect(() => {
console.log('hello')
changeState()
}, [auctionStatus])
function changeState() {
// This line won't result in calling your useEffect
// setAuctionStatus("live") // 'hello' will be printed one time only.
// You need to use a state value that won't be similar to the previous one.
setAuctionStatus("inactive") // useEffect will be called and 'hello' will be printed twice.
}
}
You can simply use a flag instead that will keep on changing from true to false like this:
const [flag, setFlag] = React.useState(true)
useEffect(() => {
// ..
}, [flag])
// And in handleRenderer
getAuctionStatus().then((response) => {
setFlag(!flag);
});
Have a look at the following useCountdown hook:
https://codepen.io/AdamMorsi/pen/eYMpxOQ
const DEFAULT_TIME_IN_SECONDS = 60;
const useCountdown = ({ initialCounter, callback }) => {
const _initialCounter = initialCounter ?? DEFAULT_TIME_IN_SECONDS,
[resume, setResume] = useState(0),
[counter, setCounter] = useState(_initialCounter),
initial = useRef(_initialCounter),
intervalRef = useRef(null),
[isPause, setIsPause] = useState(false),
isStopBtnDisabled = counter === 0,
isPauseBtnDisabled = isPause || counter === 0,
isResumeBtnDisabled = !isPause;
const stopCounter = useCallback(() => {
clearInterval(intervalRef.current);
setCounter(0);
setIsPause(false);
}, []);
const startCounter = useCallback(
(seconds = initial.current) => {
intervalRef.current = setInterval(() => {
const newCounter = seconds--;
if (newCounter >= 0) {
setCounter(newCounter);
callback && callback(newCounter);
} else {
stopCounter();
}
}, 1000);
},
[stopCounter]
);
const pauseCounter = () => {
setResume(counter);
setIsPause(true);
clearInterval(intervalRef.current);
};
const resumeCounter = () => {
setResume(0);
setIsPause(false);
};
const resetCounter = useCallback(() => {
if (intervalRef.current) {
stopCounter();
}
setCounter(initial.current);
startCounter(initial.current - 1);
}, [startCounter, stopCounter]);
useEffect(() => {
resetCounter();
}, [resetCounter]);
useEffect(() => {
return () => {
stopCounter();
};
}, [stopCounter]);
return [
counter,
resetCounter,
stopCounter,
pauseCounter,
resumeCounter,
isStopBtnDisabled,
isPauseBtnDisabled,
isResumeBtnDisabled,
];
};
I need to make background change every second. if i use setinterval. the background changes too fast.
here's my code:
const { url, id, isButtonPrev, isButtonNext } = useOwnSelector(state => state.sliderReducer);
const img = useRef<HTMLImageElement>(null);
const dispatch = useOwnDispatch();
Here's function which chang background
const setBackGround = (index: number | null = null) => {
console.log(index)
if(img.current) {
img.current.src = `${url}${id < 10 ? `0${id}` : `${id}`}.jpg`;
img.current.onload = () => {
document.body.style.backgroundImage = `url(${img.current?.src})`;
if (index) dispatch(setId(index));
dispatch(isButton(''));
}
}
}
then I call this function:
setBackGround();
setInterval(() => {
setBackGround(id + 1);
}, 1000);
but background change very fast
I also tried to use the useEffect hook. But it didn’t help either
useEffect( () => {
const intervalID = setInterval(() => {
setBackGround(id + 1);
}, 1000);
return clearInterval(intervalID);
}, []);
useRef returns an object like {current: "value"}.Therefore, you need to use it as follows.
const imgRef = useRef<HTMLImageElement>(null);
if(imgRef.current){
imgRef.current.src = url;
}
I've been trying to convert the following code from React Class Component to Function Component but I've been having problems since I've gotten the error "Expected an assignment or function call and instead saw an expression. eslint no-unused-expressions"
componentDidMount() {
this.startingSequence();
}
startingSequence = () => {
setTimeout(() => {
this.setState(
() => {
return {
textMessageOne: `A wild ${this.state.enemyName} appeared!`,
enemyFaint: false
};
},
() => {
setTimeout(() => {
this.setState(
{
textMessageOne: `Go ${this.state.playerName}!`,
playerFaint: false
},
() => {
setTimeout(() => {
this.setState({
textMessageOne: ""
});
}, 3000);
}
);
}, 3000);
}
);
}, 1000);
};
This is the code I ended up with while trying to convert it to Function Component:
const startingSequence = () => {
setTimeout(() => {
() => {
setTextMessageOne(state => {
state = (`Wild ${enemyName} appeared!`)
return state;})
setEnemyFaint(state => {
state = false
return state;})
}
,
() => {
setTimeout(() => {
setTextMessageOne(`Go ${playerName}!`),
setPlayerFaint(false)
,
() => {
setTimeout(() => {
setTextMessageOne("")
}, 3000);
}
}, 3000);
}
}, 1000);
};
useEffect(() => {
startingSequence();
})
EDIT:
Solution I got thanks to Kieran Osgood:
const startingSequence = () => {
setTimeout(() => {
setTextMessageOne(`Wild ${enemyName} appeared!`)
setEnemyFaint(false)
setTimeout(() => {
setTextMessageOne(`Go ${playerName}!`)
setPlayerFaint(false)
setTimeout(() => {
setTextMessageOne('')
}, 3000)
}, 3000)
}, 1000)
}
useEffect(() => {
startingSequence()
}, [enemyFaint])
In the functional component syntax you can pass the new state in directly OR use the function syntax if you need access to the previous state, however the state variable is not assignable so when you're doing this:
setTextMessageOne(state => {
state = `Wild ${enemyName} appeared!`
return state
})
You could do it simply like this:
setTextMessageOne(`Wild ${enemyName} appeared!`)
Function syntax is helpful for lets say a counter, where we're incrementing a number, and avoids getting stale closures overlapping each other.
setCounter(previousState => {
return previousState + 1
})
// OR
setCounter(previousState => previousState + 1)
So amending that, the other issue is theres a lot of nested arrow functions which seem to stem from the previous usage of the second argument to setState which is a callback to be executed immediately after the state is set - this doesn't exist in functional components, so you should probably refactor this function to be something more along the lines of
// this is just a basic representation, consider combining these to objects etc.
const [enemyName, setEnemyName] = React.useState('')
const [enemyFaint, setEnemyFaint] = React.useState(false)
const [playerFaint, setPlayerFaint] = React.useState(false)
const [textMessageOne, setTextMessageOne] = React.useState('')
const [playerName, setPlayerName] = React.useState('')
const startingSequence = () => {
setTimeout(() => {
setTextMessageOne(state => {
state = `Wild ${enemyName} appeared!`
return state
})
setEnemyFaint(false)
}, 1000)
}
React.useEffect(() => {
setTimeout(() => {
setTextMessageOne(`Go ${playerName}!`)
setPlayerFaint(false)
setTimeout(() => {
setTextMessageOne('')
}, 3000)
}, 3000)
}, [enemyFaint])
Then you want to take these further to extract into custom hooks so its more clear your intent in the flow of your component but generally this is the way in functional components to respond to state changes, via the useEffect
I'm loading multiple animals into my ThreeJS project. All these animals have PositionalAudio with a setInterval function. I use them inside a useEffect function. On the callback I want to clear the interval, but it keeps calling the function.
This is the function where I set my setInterval:
const loadAudio = () => {
const animalSound = new THREE.PositionalAudio(listener);
animalSound.setBuffer(animalBuffer);
playSounds = setInterval(() => {
animalSound.play();
} , 5000);
audios.push(animalSound);
}
In the return function I try to clear the interval:
return () => {
audios.forEach((audio) => {
audio.stop();
clearInterval(playSounds);
});
};
Sadly the audio keeps playing every 5 seconds
Here is a code snippet
https://codesandbox.io/s/bitter-tree-bb4ld?file=/src/App.js
According to your code snippet, say you have Button:
<button
onClick={buttonToggle}
>
{start ? 'start' : 'stop'}
</button>
Initially we have some setup for useState and handle click function
const [seconds, setSeconds] = useState(0);
const [btnStart, setBtnStart] = useState(true);
const buttonToggle = useCallback(
() => setBtnStart(run => !run)
, []);
In the useEffect you will do following changes
useEffect(() => {
if(!btnStart) {
// setSeconds(0); // if you want to reset it as well
return;
}
const interval = setInterval(() => {
setSeconds(seconds => seconds + 1);
}, 1000);
return () => clearInterval(interval);
}, [btnStart]);