React hook setState, setTimeout memory leak - reactjs

import React, { useState, useEffect, useRef } from 'react';
import styles from './TextAnimation.module.scss';
const TextAnimation = () => {
const [typedText, setTypedText] = useState([
"Welcome to Byc",
"Change your Life"
]);
const [value, setValue] = useState();
const [inType, setInType] = useState(false);
let attachClasses = [styles.Blink];
if(inType) {
attachClasses.push(styles.Typing)
}
const typingDelay = 200;
const erasingDelay = 100;
const newTextDelay = 5000;
let textArrayIndex = 0;
let charIndex = 0;
const type = () => {
if(charIndex < typedText[textArrayIndex].length + 1) {
setValue(typedText[textArrayIndex].substring(0, charIndex));
charIndex ++;
setTime();
} else {
setInType(false);
setTimeout(erase, newTextDelay);
}
};
const setTime = () => {
setTimeout(type, typingDelay);
};
const erase = () => {
if(charIndex > 0) {
setValue(typedText[textArrayIndex].substring(0, charIndex - 1));
charIndex --;
setTimeout(erase, erasingDelay);
} else {
setInType(false);
textArrayIndex ++;
if(textArrayIndex >= typedText.length) {
textArrayIndex = 0;
}
setTimeout(type, newTextDelay - 3100);
}
};
useEffect(() => {
type();
}, [])
return (
<div className={styles.TextAnimation}>
<span className={styles.Text} >{value}</span><span className={attachClasses.join(' ')} > </span>
</div>
);
};
export default TextAnimation;
I'am trying to make text animation, but i got an message just like this...
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
How can i fix it?

You need to clear timeouts when your component unmounts, otherwise maybe a timeout will run after the component is unmounted.
To do that :
store the return value of each timeout in a list in some ref (with React.useRef for example)
return a callback in useEffect that clears the timeouts with clearTimeout(<return value of setTimeout>)

Related

How to stop useState hook setState re-render in the useEffect? I am trying to code a count down function and the mins can sync from another component

I am trying to code a countdown clock and the "mins" is from a brother component.
If I take off setMin from useEffect then it is not changing while the value change in source components, but if I leave it in useEffect it will re-rendering every time if the seconds change and makes it immutable. Anyway, can I fix this problem?
If I need useRef how can I use it with setMin?
export default function Timer(props) {
const { count } = props;
const [isPlay, setIsPlay] = useState(false);
const [isRestore, setIsRestore] = useState(false);
const [second, setSecond] = useState(0);
const [isBreak, setIsBreak] = useState(false);
const [min, setMin] = useState(count)
useEffect(() => {
setMin(count)
let alarm = document.getElementById("beep");
const countdown = () => {
setTimeout(() => {
if (second === 0 && min > 0) {
setMin(min-1);
setSecond(59);
} else if (min >= 0 && second > 0) {
setSecond(second - 1);
} else {
alarm.play();
alarm.addEventListener("ended", () => {
setIsBreak(!isBreak);
});
}
}, 1000);
};
if (isPlay) {
countdown();
} else {
alarm.pause();
}
if (isRestore) {
setIsPlay(false);
alarm.currentTime = 0;
setIsRestore(false);
}
},[isPlay, isRestore, second, isBreak,min,count]);
I think the majority of your logic should be outside of the useEffect hook. Take a look at this answer Countdown timer in React, it accomplishes pretty much the same task as yours and should help you get an idea of the necessary logic

React function on pageload

I want the heartsDisplay function call on pageload, but doing it like that causes an error. It works only with on click. How do I do this in React?
Or maybe there is a way to add default value for hearts in useState hook?
import React, { useState } from 'react'
import './App.css';
var heartsNum = 3;
const App = () => {
const [hearts, setHearts] = useState("");
var Score = 0;
var customColor = {color: 'red'};
const heartsDisplay = () => {
if (heartsNum === 3) {
setHearts("Hearts: ❤❤❤");
} else if (heartsNum === 2) {
setHearts("Hearts: ❤❤");
} else if (heartsNum === 1) {
setHearts("Hearts: ❤");
} else if (heartsNum < 1) {
setHearts("Hearts: ");
}
};
heartsDisplay();
const changeHearts = () => {
heartsNum = heartsNum - 1;
console.log(heartsNum);
heartsDisplay();
}
return (
<div>
<h3 className='hearts'>{hearts}</h3>
<button className='col1' onClick={changeHearts}>Click</button>
</div>
)
}
export default App
useEffect(()=>{
heartsDisplay();
},[]);
Call your function inside useEffect() hook
The useEffect Hook allows you to perform side effects in your components.
Some examples of side effects are: fetching data, directly updating the DOM, and timers.
useEffect accepts two arguments. The second argument is optional.
useEffect(<function>, <dependency>)
https://reactjs.org/docs/hooks-effect.html
import React, { useState } from 'react'
import './App.css';
var heartsNum = 3;
const App = () => {
const [hearts, setHearts] = useState("");
var Score = 0;
var customColor = {color: 'red'};
const heartsDisplay = () => {
if (heartsNum === 3) {
setHearts("Hearts: ❤❤❤");
} else if (heartsNum === 2) {
setHearts("Hearts: ❤❤");
} else if (heartsNum === 1) {
setHearts("Hearts: ❤");
} else if (heartsNum < 1) {
setHearts("Hearts: ");
}
};
call the function inside useEffect hook with no deps to run this function one time to trigger when a change in state or props put that state or props in deps array if you want to trigger the function before unmount return a function in useEffect callback do it in that function if you call the function openly in the component function it will call in all render
useEffect(() => {
heartsDisplay();
},[]);
const changeHearts = () => {
heartsNum = heartsNum - 1;
console.log(heartsNum);
heartsDisplay();
}
return (
<div>
<h3 className='hearts'>{hearts}</h3>
<button className='col1' onClick={changeHearts}>Click</button>
</div>
)
}
export default App
You are misunderstanding the use of useState. Default value for useState is the default value for the hearts variable.
What you are looking for is probably the useEffect hook.
It's default behavior is
similar to componentDidMount and componentDidUpdate
which basically leads to on page load behavior.
import React, { useState, useEffect } from 'react'
import './App.css';
var heartsNum = 3;
const App = () => {
const [hearts, setHearts] = useState("");
var Score = 0;
var customColor = {color: 'red'};
useEffect(() => {
heartsDisplay();
},[]);
const heartsDisplay = () => {
if (heartsNum === 3) {
setHearts("Hearts: ❤❤❤");
} else if (heartsNum === 2) {
setHearts("Hearts: ❤❤");
} else if (heartsNum === 1) {
setHearts("Hearts: ❤");
} else if (heartsNum < 1) {
setHearts("Hearts: ");
}
};
const changeHearts = () => {
heartsNum-=1;
console.log(heartsNum);
heartsDisplay();
}
return (
<div>
<div></div>
<h3 className='hearts'>{hearts}</h3>
<button className='col1' onClick={changeHearts}>Click</button>
</div>
)
}
export default App

React useRef() with setInterval() in useCallBack() function not clearing upon clearInterval()

I have this stopwatch code, which shows the elapsed time on screen. (React Component)
It all works okay, but calling "clearInterval(increment.current)" (which using using a useRef() for scope in React) doesn't seem to really "stop" the Interval. It keeps logging "Triggered" in the console (every second) - and I don't follow why it's still being called after clearInterval() when currentActivityOn === false. (And if the timer is triggered several times, it triggers more than once per second.)
Any suggestions?
import React, { useEffect, useRef, useCallback, useContext } from 'react'
import Container from '#material-ui/core/Container'
import Typography from '#material-ui/core/Typography'
import useLocalStorage from '../../hooks/useLocalStorage'
import { FlowTimeContext } from '../../services/flow/flow-time.context'
import { useStyles } from './grid-stopwatch.styles'
export default function GridStopwatch() {
const classes = useStyles()
const { currentActivityOn } = useContext(FlowTimeContext)
const [timerStartTime, setTimerStartTime] = useLocalStorage('timerStartTime', '')
const [timer, setTimer] = useLocalStorage('timer', 0)
const increment = useRef(null)
const handleTimerRun = useCallback(() => {
increment.current = setInterval(() => {
if(timerStartTime !== ''){
const new_timer = ((Math.floor(new Date().getTime() / 1000)) - timerStartTime)
setTimer(new_timer)
} else {
console.log("Triggered") // Why is this being called continually?
}
}, 1000)
}, [setTimer, timerStartTime])
const handleReset = useCallback(() => {
clearInterval(increment.current)
setTimer(0)
setTimerStartTime('')
}, [setTimer, setTimerStartTime])
const handleStart = useCallback(() => {
if(timerStartTime === ''){
var start = Math.floor(new Date().getTime() / 1000)
setTimerStartTime(start)
}
}, [timerStartTime, setTimerStartTime])
const formatTime = () => {
const getSeconds = `0${timer % 60}`.slice(-2)
const minutes = `${Math.floor(timer / 60)}`
const getMinutes = `0${minutes % 60}`.slice(-2)
const getHours = `0${Math.floor(timer / 3600)}`.slice(-2)
return `${getHours}h : ${getMinutes}m : ${getSeconds}s`
}
useEffect(() => {
if(currentActivityOn === true){
handleStart()
handleTimerRun()
} else {
handleReset()
}
}, [currentActivityOn, handleStart, handleReset, handleTimerRun])
return (
<Container maxWidth='xl' className={classes.container}>
{currentActivityOn && (
<Typography variant='subtitle2' gutterBottom>
Current Activity Duration: {formatTime()}
</Typography>
)}
</Container>
)
}
So the useEffect gets triggered twice in this case. So handleTimerRun got called twice, and it seems each time there was an additional interval started that was never cleared.
const handleTimerRun = useCallback(() => {
if(timerStartTime !== ''){
increment.current = setInterval(() => {
const new_timer = ((Math.floor(new Date().getTime() / 1000)) - timerStartTime)
setTimer(new_timer)
}, 1000)
}
}, [setTimer, timerStartTime, increment])
For now I'm able to work around this by moving the condition before starting the interval, so it only gets started once. This seems to solve my issue, but is a workaround rather than truly understanding "how" there can be an additional interval started that is assigned the same id. (and seemingly can't be "stopped")
Further clarity on that would certainly be appreciated.
It seems that if I console log increment.current, it keeps increasing - whereas I thought it would be a consistent ID...? (mutable yes, but consistent when not being mutated.)

How can I reference the originally created hook in React?

I have the following usage of my hook, but it doesn't use the new timerDuration when I update inside my input:
const [secondsBetweenRepsSetting, setSecondsBetweenRepsSetting] = useState(DEFAULT_SECONDS_BETWEEN_REPS)
const {secondsLeft, isRunning, start, stop} = useTimer({
duration: secondsBetweenRepsSetting,
onExpire: () => sayRandomExerciseName(),
onTick: () => handleTick(),
});
const onTimeBetweenRepsChange = (event: any) => {
const secondsBetweenRepsSettingString = event.target.value;
const secondsBetweenRepsSettingInt = parseInt(secondsBetweenRepsSettingString)
setSecondsBetweenRepsSetting(secondsBetweenRepsSettingInt)
}
return <React.Fragment>
<input type="number" name="secondsBetweenRepsSetting" value={secondsBetweenRepsSetting} onChange={onTimeBetweenRepsChange}/>
</React.Fragment>
And here is the implementation of the useTimer hook, which I'm not sure why it's not getting my duration update?
import { useState } from 'react';
import Validate from "../utils/Validate";
import useInterval from "./useInterval";
export default function useTimer({ duration: timerDuration, onExpire, onTick}) {
const [duration] = useState(timerDuration)
const [secondsLeft, setSecondsLeft] = useState(timerDuration)
const [isRunning, setIsRunning] = useState(false)
function start() {
setIsRunning(true)
}
function stop() {
setIsRunning(false)
}
function handleExpire() {
Validate.onExpire(onExpire) && onExpire();
}
useInterval(() => {
const secondsMinusOne = secondsLeft - 1;
setSecondsLeft(secondsMinusOne)
if(secondsMinusOne <= 0) {
setSecondsLeft(duration) // Reset timer automatically
handleExpire()
} else {
Validate.onTick(onTick) && onTick();
}
}, isRunning ? 1000 : null)
return {secondsLeft, isRunning, start, stop, }
}
Remove the line:
const [duration] = useState(timerDuration);
You are already getting duration from timerDuration, just use that.

How to get value from useState inside the function

I am trying to build Hanging man game and want to get value from useState inside the checkMatchLetter function, but not sure if that is possible and what I did wrong....
import React, { useState, useEffect } from 'react';
import { fetchButton } from '../actions';
import axios from 'axios';
import 'babel-polyfill';
const App = () => {
const [word, setWord] = useState([]);
const [underscore, setUnderscore] = useState([]);
const [data, setData] = useState([]);
useEffect(() => {
const runEffect = async () => {
const result = await axios('src/api/api.js');
setData(result.data)
}
runEffect();
}, []);
const randomWord = () => {
const chosenWord = data[Math.floor(Math.random() * data.length)];
replaceLetter(chosenWord.word);
}
const replaceLetter = (string) => {
let getString = string; // here it shows a valid string.
setWord(getString);
let stringToUnderScore = getString.replace(/[a-z]/gi, '_');
setUnderscore(stringToUnderScore);
}
useEffect(() => {
const checkLetter = (event) => {
if(event.keyCode >= 65 && event.keyCode <= 90) {
checkMatchLetter(word, String.fromCharCode(event.keyCode).toLowerCase());
}
};
document.addEventListener('keydown', checkLetter);
return () => {
document.removeEventListener('keydown', checkLetter);
}
}, []);
const checkMatchLetter = (keyButton) => {
console.log(keyButton);
let wordLength = word.length;
console.log(wordLength); // here it outputs '0'
/// here I want word of useState here....
}
return (
<div>
<p>{word}</p>
<p>{underscore}</p>
<button onClick={randomWord}></button>
</div>
)
}
export default App;
The reason why I want to obtain that value inside this function is so I can compare the clicked keybutton (a-z) to the current chosenword. And if there is something wrong with other functions, please feel free to share your feedback here below as well.
You're using a variable defined inside the component render function in a useEffect effect and that variable is missing in the hook's deps. Always include the deps you need (I highly recommend the lint rule react-hooks/exhaustive-deps). When you add checkMatchLetter to deps you'll always have the newest instance of the function inside your effect instead of always using the old version from the first render like you do now.
useEffect(() => {
const checkLetter = (event) => {
if(event.keyCode >= 65 && event.keyCode <= 90) {
checkMatchLetter(word, String.fromCharCode(event.keyCode).toLowerCase());
}
};
document.addEventListener('keydown', checkLetter);
return () => {
document.removeEventListener('keydown', checkLetter);
}
}, [checkMatchLetter, word]);
This change will make the effect run on every render. To rectify that, you can memoise your callbacks. However, that's a new can of worms.

Resources