I'm trying out React Native by implementing a simple timer. When running the code, the counting works perfectly for the first 6 seconds, where then the app starts to act weird.
Here is the code, which you can try in this Expo Snack
import React, { useEffect, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Button } from 'react-native';
const App = () => {
return (
<View style={styles.container}>
<Timer></Timer>
</View>
);
}
const Timer = (props) => {
const [workTime, setWorkTime] = useState({v: new Date()});
const [counter, setCounter] = useState(0);
setInterval(() => {
setCounter( counter + 1);
setWorkTime({v:new Date(counter)});
}, 1000);
return (
<View>
<Text>{workTime.v.toISOString()}</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
export default App;
For each new component rerender, React create a new interval, which results in a memory leak and unexpected behavior.
Let us create a custom hook for interval
import { useEffect, useLayoutEffect, useRef } from 'react'
function useInterval(callback, delay) {
const savedCallback = useRef(callback)
// Remember the latest callback if it changes.
useLayoutEffect(() => {
savedCallback.current = callback
}, [callback])
// Set up the interval.
useEffect(() => {
// Don't schedule if no delay is specified.
// Note: 0 is a valid value for delay.
if (!delay && delay !== 0) {
return
}
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}, [delay])
}
export default useInterval
Use it in functional component
const Timer = (props) => {
const [workTime, setWorkTime] = useState({v: new Date()});
const [counter, setCounter] = useState(0);
useInterval(()=>{
setCounter( counter + 1);
setWorkTime({v:new Date(counter)});
},1000)
return (
<View>
<Text>{workTime.v.toISOString()}</Text>
</View>
)
}
Here tested result - https://snack.expo.dev/#emmbyiringiro/58cf4b
As #AKX commented, try wrapping your interval code in a useEffect. Also, return a function from it that clears the interval (the cleanup function).
useEffect(() => {
const interval = setInterval(() => {
setCounter( counter + 1);
setWorkTime({v:new Date(counter)});
}, 1000);
return () => clearInterval(interval);
});
However, using setInterval with 1000 second delay does not yield an accurate clock, if that's what you're going for.
Related
I am trying to build a modal which, when opened, only displays for ten seconds and then stops showing after that time.
I have tried to code this mechanism using setInterval and useState.
At the moment, this only works the first time when I click "Open Modal".
Each subsequent time I click "Open Modal", the timer messes up and counts down from less and less time and only displays for 5...then 2.. then 1 second.
After googling, everyone has suggested adding "ClearInterval" to the useEffect hook but that has not worked for me.
import gsap from 'gsap'
import Image from 'next/future/image'
import { type FC, useEffect, useState } from 'react'
import { useRef } from 'react'
import { Transition } from 'react-transition-group'
import logo from '#/assets/brand/logo.svg'
import styles from './Modal.module.scss'
const Modal: FC<any> = ({ show, setShow }) => {
const container = useRef<HTMLDivElement>(null)
const q = gsap.utils.selector(container)
const [timeLeft, setTimeLeft] = useState(10)
useEffect(() => {
const intervalId = setInterval(() => {
setTimeLeft((prev) => prev - 1)
}, 1000)
if (!show) return
() => {
clearInterval(intervalId)
}
}, [show])
useEffect(() => {
if (timeLeft > 0) return
setShow(false)
setTimeLeft(10)
}, [setShow, timeLeft])
const onEnter = () => {
gsap.set(container.current, { opacity: 0 })
gsap.set(q('*'), { opacity: 0, y: 8 })
}
const onEntering = () => {
gsap.timeline().to(container.current, { opacity: 1, duration: 0.3 }).to(q('*'), { opacity: 1, y: 0, stagger: 0.08 })
}
const onExiting = () => {
gsap.to(container.current, { autoAlpha: 0, duration: 0.3 })
}
return (
<Transition
in={show}
timeout={600}
mountOnEnter={true}
unmountOnExit={true}
nodeRef={container}
onEnter={onEnter}
onEntering={onEntering}
onExiting={onExiting}>
<div ref={container} className={styles.container}>
<Image src={logo} alt="Loopspeed" width={214} height={44} style={{ objectFit: 'contain' }} />
<p>Blah Blah </p>
This modal will close in <strong>{timeLeft}</strong> seconds.
</p>
</div>
</Transition>
)
}
export default Modal
Please let me know if I need to post any more code.
I have tried using setInterval and useState, and also setInterval, clearInterval and useState.
Just a misc error with useEffect - if you setInterval in the useEffect - ensure you are clearing it, for useEffect hook the "cleanup" function needs to be returned - return () => ....
useEffect(() => {
if (!show) return;
const intervalId = setInterval(() => {
setTimeLeft((prev) => prev - 1)
}, 1000)
return () => {
clearInterval(intervalId)
}
}, [show]);
I learning javascript, react, and i tried to count from 10 to 0, but somehow the timer only run to 9, i thought setInterval run every n time we set (n can be 1000ms, 2000ms...)
Here is the code
import "./styles.css";
import React, { useState } from "react";
export default function App() {
const [time, setTime] = useState(10);
const startCountDown = () => {
const countdown = setInterval(() => {
setTime(time - 1);
}, 1000);
if (time === 0) {
clearInterval(countdown);
}
};
return (
<div>
<button
onClick={() => {
startCountDown();
}}
>
Start countdown
</button>
<div>{time}</div>
</div>
);
}
Here is the code:
https://codesandbox.io/s/class-component-ujc9s?file=/src/App.tsx:0-506
Please explain for this, i'm so confuse, thank you
time is the value read from the state (which is the default passed passed into useState) each time the component renders.
When you click, you call setInterval with a function that closes over the time that came from the last render
Every time the component is rendered from then on, it reads a new value of time from the state.
The interval is still working with the original variable though, which is still 10.
State functions will give you the current value of the state if you pass in a callback. So use that instead of the closed over variable.
setTime(currentTime => currentTime - 1);
Just use callback in your setState function because otherwise react is working with old value of time:
import "./styles.css";
import React, { useState } from "react";
export default function App() {
const [time, setTime] = useState(10);
const startCountDown = () => {
const countdown = setInterval(() => {
setTime((prevTime)=>prevTime - 1);
}, 1000);
if (time === 0) {
clearInterval(countdown);
}
};
return (
<div>
<button
onClick={() => {
startCountDown();
}}
>
Start countdown
</button>
<div>{time}</div>
</div>
);
}
edit: You can store your interval identificator in useRef, because useRef persist through rerender and it will not cause another rerender, and then check for time in useEffect with time in dependency
import "./styles.css";
import React, { useEffect, useRef, useState } from "react";
export default function App() {
const [time, setTime] = useState(10);
const interval = useRef(0);
const startCountDown = () => {
interval.current = setInterval(() => {
setTime((prevTime) => prevTime - 1);
}, 1000);
};
useEffect(() => {
if (time === 0) {
clearInterval(interval.current);
}
}, [time]);
return (
<div>
<button
onClick={() => {
startCountDown();
}}
>
Start countdown
</button>
<div>{time}</div>
</div>
);
}
working sandbox: https://codesandbox.io/s/class-component-forked-lgiyj?file=/src/App.tsx:0-613
I have a here a input field that on every type, it dispatches a redux action.
I have put a useDebounce in order that it won't be very heavy. The problem is that it says Hooks can only be called inside of the body of a function component. What is the proper way to do it?
useTimeout
import { useCallback, useEffect, useRef } from "react";
export default function useTimeout(callback, delay) {
const callbackRef = useRef(callback);
const timeoutRef = useRef();
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
const set = useCallback(() => {
timeoutRef.current = setTimeout(() => callbackRef.current(), delay);
}, [delay]);
const clear = useCallback(() => {
timeoutRef.current && clearTimeout(timeoutRef.current);
}, []);
useEffect(() => {
set();
return clear;
}, [delay, set, clear]);
const reset = useCallback(() => {
clear();
set();
}, [clear, set]);
return { reset, clear };
}
useDebounce
import { useEffect } from "react";
import useTimeout from "./useTimeout";
export default function useDebounce(callback, delay, dependencies) {
const { reset, clear } = useTimeout(callback, delay);
useEffect(reset, [...dependencies, reset]);
useEffect(clear, []);
}
Form component
import React from "react";
import TextField from "#mui/material/TextField";
import useDebounce from "../hooks/useDebounce";
export default function ProductInputs(props) {
const { handleChangeProductName = () => {} } = props;
return (
<TextField
fullWidth
label="Name"
variant="outlined"
size="small"
name="productName"
value={formik.values.productName}
helperText={formik.touched.productName ? formik.errors.productName : ""}
error={formik.touched.productName && Boolean(formik.errors.productName)}
onChange={(e) => {
formik.setFieldValue("productName", e.target.value);
useDebounce(() => handleChangeProductName(e.target.value), 1000, [
e.target.value,
]);
}}
/>
);
}
I don't think React hooks are a good fit for a throttle or debounce function. From what I understand of your question you effectively want to debounce the handleChangeProductName function.
Here's a simple higher order function you can use to decorate a callback function with to debounce it. If the returned function is invoked again before the timeout expires then the timeout is cleared and reinstantiated. Only when the timeout expires is the decorated function then invoked and passed the arguments.
const debounce = (fn, delay) => {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
}
};
Example usage:
export default function ProductInputs({ handleChangeProductName }) {
const debouncedHandler = useCallback(
debounce(handleChangeProductName, 200),
[handleChangeProductName]
);
return (
<TextField
fullWidth
label="Name"
variant="outlined"
size="small"
name="productName"
value={formik.values.productName}
helperText={formik.touched.productName ? formik.errors.productName : ""}
error={formik.touched.productName && Boolean(formik.errors.productName)}
onChange={(e) => {
formik.setFieldValue("productName", e.target.value);
debouncedHandler(e.target.value);
}}
/>
);
}
If possible the parent component passing the handleChangeProductName callback as a prop should probably handle creating a debounced, memoized handler, but the above should work as well.
Taking a look at your implementation of useDebounce, and it doesn't look very useful as a hook. It seems to have taken over the job of calling your function, and doesn't return anything, but most of it's implementation is being done in useTimeout, which also not doing much...
In my opinion, useDebounce should return a "debounced" version of callback
Here is my take on useDebounce:
export default function useDebounce(callback, delay) {
const [debounceReady, setDebounceReady] = useState(true);
const debouncedCallback = useCallback((...args) => {
if (debounceReady) {
callback(...args);
setDebounceReady(false);
}
}, [debounceReady, callback]);
useEffect(() => {
if (debounceReady) {
return undefined;
}
const interval = setTimeout(() => setDebounceReady(true), delay);
return () => clearTimeout(interval);
}, [debounceReady, delay]);
return debouncedCallback;
}
Usage will look something like:
import React from "react";
import TextField from "#mui/material/TextField";
import useDebounce from "../hooks/useDebounce";
export default function ProductInputs(props) {
const handleChangeProductName = useCallback((value) => {
if (props.handleChangeProductName) {
props.handleChangeProductName(value);
} else {
// do something else...
};
}, [props.handleChangeProductName]);
const debouncedHandleChangeProductName = useDebounce(handleChangeProductName, 1000);
return (
<TextField
fullWidth
label="Name"
variant="outlined"
size="small"
name="productName"
value={formik.values.productName}
helperText={formik.touched.productName ? formik.errors.productName : ""}
error={formik.touched.productName && Boolean(formik.errors.productName)}
onChange={(e) => {
formik.setFieldValue("productName", e.target.value);
debouncedHandleChangeProductName(e.target.value);
}}
/>
);
}
Debouncing onChange itself has caveats. Say, it must be uncontrolled component, since debouncing onChange on controlled component would cause annoying lags on typing.
Another pitfall, we might need to do something immediately and to do something else after a delay. Say, immediately display loading indicator instead of (obsolete) search results after any change, but send actual request only after user stops typing.
With all this in mind, instead of debouncing callback I propose to debounce sync-up through useEffect:
const [text, setText] = useState('');
const isValueSettled = useIsSettled(text);
useEffect(() => {
if (isValueSettled) {
props.onChange(text);
}
}, [text, isValueSettled]);
...
<input value={value} onChange={({ target: { value } }) => setText(value)}
And useIsSetlled itself will debounce:
function useIsSettled(value, delay = 500) {
const [isSettled, setIsSettled] = useState(true);
const isFirstRun = useRef(true);
const prevValueRef = useRef(value);
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
setIsSettled(false);
prevValueRef.current = value;
const timerId = setTimeout(() => {
setIsSettled(true);
}, delay);
return () => { clearTimeout(timerId); }
}, [delay, value]);
if (isFirstRun.current) {
return true;
}
return isSettled && prevValueRef.current === value;
}
where isFirstRun is obviously save us from getting "oh, no, user changed something" after initial rendering(when value is changed from undefined to initial value).
And prevValueRef.current === value is not required part but makes us sure we will get useIsSettled returning false in the same render run, not in next, only after useEffect executed.
I am trying to build a simple stopwatch app in react-native. I'm using AsyncStorage to store the time recorded data into local storage, along with that I would like to display a table that shows all the recorded times. The core idea is that when a person presses and holds a LottieView animation, it will start a timer, when they press out, the timer stops, records in AsyncStorage and then updates the table.
After 10 elements, my FlatList (inside TimeTable.jsx) becomes extremely slow and I am not sure why. The component that is causing this error is I believe TimeTable.jsx but I am not quite sure why.
src/components/Timer/TimeTable.jsx
import React, {useState, useEffect} from 'react'
import { StyleSheet, FlatList } from "react-native";
import { Divider, List, ListItem } from '#ui-kitten/components'
import AsyncStorage from '#react-native-async-storage/async-storage';
const getRecordedEventsTable = async (dbKey) => {
try {
let currentDataArray = await AsyncStorage.getItem(dbKey);
return currentDataArray ? JSON.parse(currentDataArray) : [];
} catch (err) {
console.log(err);
}
};
const renderItem = ({ item, index }) => (
<ListItem
title={`${item.timeRecorded / 1000} ${index + 1}`}
description={`${new Date(item.timestamp)} ${index + 1}`}
/>
)
export const TimeTable = ({storageKey, timerOn}) => {
const [timeArr, setTimeArr] = useState([]);
useEffect(() => {
getRecordedEventsTable(storageKey).then((res) => {
setTimeArr(res)
})
}, [timerOn])
return (
<FlatList
style={styles.container}
data={timeArr}
ItemSeparatorComponent={Divider}
renderItem={renderItem}
keyExtractor={item => item.timestamp.toString()}
/>
);
};
const styles = StyleSheet.create({
container: {
maxHeight: 200,
},
});
src/components/Timer/Timer.jsx
import React, {useState, useEffect, useRef} from 'react'
import {
View,
StyleSheet,
Pressable,
} from 'react-native';
import {Layout, Button, Text} from '#ui-kitten/components';
import LottieView from 'lottie-react-native'
import AsyncStorage from '#react-native-async-storage/async-storage';
import {TimeTable} from './TimeTable'
const STORAGE_KEY = 'dataArray'
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#E8EDFF"
},
seconds: {
fontSize: 40,
paddingBottom: 50,
}
})
const getRecordedEventsTable = async () => {
try {
let currentDataArray = await AsyncStorage.getItem(STORAGE_KEY)
return currentDataArray ? JSON.parse(currentDataArray) : []
} catch (err) {
console.log(err)
}
}
const addToRecordedEventsTable = async (item) => {
try {
let dataArray = await getRecordedEventsTable()
dataArray.push(item)
await AsyncStorage.setItem(
STORAGE_KEY,
JSON.stringify(dataArray)
)
} catch (err) {
console.log(err)
}
}
// ...
const Timer = () => {
const [isTimerOn, setTimerOn] = useState(false)
const [runningTime, setRunningTime] = useState(0)
const animation = useRef(null);
const handleOnPressOut = () => {
setTimerOn(false)
addToRecordedEventsTable({
timestamp: Date.now(),
timeRecorded: runningTime
})
setRunningTime(0)
}
useEffect(() => {
let timer = null
if(isTimerOn) {
animation.current.play()
const startTime = Date.now() - runningTime
timer = setInterval(() => {
setRunningTime(Date.now() - startTime)
})
} else if(!isTimerOn) {
animation.current.reset()
clearInterval(timer)
}
return () => clearInterval(timer)
}, [isTimerOn])
return (
<View>
<Pressable onPressIn={() => setTimerOn(true)} onPressOut={handleOnPressOut}>
<LottieView ref={animation} style={{width: 300, height: 300}} source={require('../../../assets/record.json')} speed={1.5}/>
</Pressable>
<Text style={styles.seconds}>{runningTime/1000}</Text>
<TimeTable storageKey={STORAGE_KEY} timerOn={isTimerOn} />
<Button onPress={resetAsyncStorage}>Reset Async</Button>
</View>
)
}
export default Timer
Any help, appreciated. Thanks.
EDIT:
Received the following warning in console:
VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc. Object {
"contentLength": 1362.5,
"dt": 25161,
"prevDt": 368776,
EDIT:
In Timer.jsx, I have a Text View in the render function as follows:
<Text style={styles.seconds}>{runningTime/1000}</Text>, this part is supposed to show the stopwatch value and update with the timer.
As the FlatList gets bigger, this is the part that becomes extremely laggy.
My suspicion is that as this is trying to re-render constantly, the children component TimeTable.jsx is also re-rendering constantly?
Looks to me like you have a loop here:
useEffect(() => {
getRecordedEventsTable(storageKey).then((res) => {
setTimeArr(res)
})
}, [timeArr, timerOn])
useEffect will get called every time timeArr is updated. Then, inside you call your async getRecordedEventsTable, and every time that finishes, it'll call setTimeArr, which will set timeArr, triggering the sequence to start again.
For optimizing the FlatList you can use different parameters that are available. You can read this https://reactnative.dev/docs/optimizing-flatlist-configuration.
Also you might consider using useCallback hook for renderItems function.
I would recommend reading this https://medium.com/swlh/how-to-use-flatlist-with-hooks-in-react-native-and-some-optimization-configs-7bf4d02c59a0
I was able to solve this problem.
The main culprit for the slowness was that in the parent component Timer.jsx because the timerOn props is changing everytime the user presses the button, the whole children component is trying to re-render and that AsyncStorage call is being called everytime. This is the reason that the {runningTime/1000} is rendering very slowly. Because everytime the timerOn component changes all child components have been queued to re-render.
The solution for this was to render the Table component from a parent of Timer and not inside the Timer component and maintain a state in Timer which is passed back to the parent and then passed to the Table component.
This is what my parent component looks like now:
const [timerStateChanged, setTimerStateChanged] = useState(false);
return (
<View style={styles.container}>
<Timer setTimerStateChanged={setTimerStateChanged} />
<View
style={{
borderBottomColor: "grey",
borderBottomWidth: 1,
}}
/>
<TimeTable timerOn={timerStateChanged} />
</View>
);
};
A better way would be to use something like React context or Redux.
Thanks for all the help.
I've built a countdown counter using React hooks, but while I was comparing its accuracy with its vanilla JS counterpart (I was displaying their current timer on the document title, i.e., I was active on a third tab), I noticed that the react timer stopped after awhile , and when I opened the tab, the page refreshed and the timer reset to its initial state, while this didn't/doesn't happen in the vanilla JS version. I would like to mention that I opened maybe 15 YouTube videos, because I want the timer to be working while doing heavy duty work on my machine. How can I prevent this from happening?
Here is the code
App.js
import React, { useState } from 'react';
import convertTime from '../helper-functions/convertTime';
import useInterval from '../hooks/useInterval';
const Countdown = () => {
const [count, setCount] = useState(82.5 * 60);
const [delay, setDelay] = useState(1000);
const [isPlaying, setIsPlaying] = useState(false);
document.title = convertTime(count);
useInterval(() => setCount(count - 1), isPlaying ? delay : null);
const handlePlayClick = () => setIsPlaying(true);
const handlePauseClick = () => setIsPlaying(false);
const handleResetClick = () => {
setCount(82.5 * 60);
setDelay(1000);
setIsPlaying(false);
};
return (
<div className='counter'>
<div className='time'>{convertTime(count)}</div>
<div className='actions'>
<button onClick={handlePlayClick}>play</button>
<button onClick={handlePauseClick}>pause</button>
<button onClick={handleResetClick}>reset</button>
</div>
</div>
);
};
export default Countdown;
useInterval.js
import React, { useState, useEffect, useRef } from 'react';
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
export default useInterval;