I was trying to build a simple counter application, the app starts counting (+1/sec) when I click the start button and stops when I click the stop button.
I came up with 2 different solutions, one using setTimeout and the other using a for loop with delay. both these solutions work for incrementing the number. Are both of these valid react ways for creating a counter? is there a better way to do this?
When stopping the app I can stop it by changing the reference variable halt.current = true but changing the state setStop(true) does nothing, why is that?
function App() {
const [get, set] = useState(0);
const [stop, setStop] = useState(false);
const halt = useRef(false);
const loopfn = async () => {
halt.current = false;
setStop(false);
while (true) {
if (halt.current || stop) break;
await new Promise((res) => setTimeout(res, 1000));
set((prev: number) => prev + 1);
}
};
const timeoutloopfn = () => {
halt.current = false;
setStop(false);
setTimeout(() => {
if (halt.current || stop) return;
set((prev: number) => prev + 1);
timeoutloopfn();
}, 1000);
};
const stoploopref = () => {
halt.current = true;
};
const stoploopst = () => {
setStop((prev: boolean) => true);
};
return (
<div>
<button onClick={loopfn}>for-loop increment</button>
<button onClick={timeoutloopfn}>timeout increment</button>
<button onClick={stoploopref}>stop using ref</button>
<button onClick={stoploopst}>stop using state</button>
<button onClick={() => set(0)}>reset</button>
<p>{get}</p>
</div>
);
}
You may consider using the setInterval function instead, storing its id in a state and clearing it when stop is set to true:
function App() {
const [get, set] = useState(0);
const [stop, setStop] = useState(false);
const [intervalId, setIntervalId] = useState(-1);
const halt = useRef(false);
useEffect(() => {
// Stop the loop
if (stop && intervalId !== -1) {
clearInterval(intervalId)
setIntervalId(-1)
}
}, [stop])
const timeoutloopfn = () => {
halt.current = false;
setStop(false);
const newIntervalId = setInterval(() => {
set((prev: number) => prev + 1);
}, 1000);
setIntervalId(newIntervalId)
};
I would totally recommend useEffect for every function that requires timing.
import React, { useRef, useEffect, useState } from "react";
export default function App() {
const [number, setNumber] = useState(100);
let intervalRef = useRef();
const decreaseNum = () => setNumber(prev => prev - 1);
useEffect(() => {
intervalRef.current = setInterval(decreaseNum, 1000);
return () => clearInterval(intervalRef.current);
}, []);
return <div>{number}</div>;
}
So I wanted to add "whatsapp" like voice note feature in my app I am working on, where you record the voice note and click the "Send" button to send it. I have added the voice recorder code and its working fine in my Logs, but the problem is that when I press the "Send recording" button it sends an empty file in the chat box, and on pressing the same button the second time it then actually sends the recorded Voice note.
The Recorder code component "useRecorder"
import { useEffect, useState } from "react";
import Conversation_Logs from "../Logs/Conversation_Logs";
const useRecorder = () => {
const [audioURL, setAudioURL] = useState("");
const [audioBlob, setAudioBlob] = useState("");
const [isRecording, setIsRecording] = useState(false);
const [recorder, setRecorder] = useState(null);
useEffect(() => {
// Lazily obtain recorder first time we're recording.
if (recorder === null) {
if (isRecording) {
requestRecorder().then(setRecorder, console.error);
}
return;
}
// Manage recorder state.
if (isRecording) {
recorder.start();
} else {
recorder.stop();
}
// Obtain the audio when ready.
const handleData = e => {
setAudioURL(URL.createObjectURL(e.data));
let wavfromblob = new File([e.data], "incomingaudioclip.wav")
setAudioBlob(wavfromblob);
};
recorder.addEventListener("dataavailable", handleData);
return () => recorder.removeEventListener("dataavailable", handleData);
}, [recorder, isRecording]);
const startRecording = () => {
setIsRecording(true);
};
const stopRecording = () => {
setIsRecording(false);
};
return [audioURL,audioBlob, isRecording, startRecording, stopRecording];
};
async function requestRecorder() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
return new MediaRecorder(stream);
}
export default useRecorder;
The Code where send recording button is called component named "RecorderBox
import React, {useState,useEffect} from "react";
import { render } from "react-dom";
import useRecorder from "./useRecorder";
const RecorderBox = (props) =>{
const { log, handleUpdateLog,handleSubmitNewMessage ,selectedLog} = props
let [audioURL,audioBlob, isRecording, startRecording, stopRecording] = useRecorder();
const [ newMessage, setNewMessage ] = useState("");
const [ attachFile, setAttachFile ] = useState();
const submitNewMessage = () => {
setAttachFile(audioBlob)
const body = {
body: newMessage,
attachment: attachFile
}
handleUpdateLog(log,newMessage, attachFile)
console.log(attachFile)
// handleSubmitNewMessage(log.id,body)
}
useEffect(() => {
setNewMessage("")
setAttachFile(null)
!!document.getElementsByClassName("dzu-previewButton")[0] && document.getElementsByClassName("dzu-previewButton")[0].click()
}, [log])
return (
<div className="RecorderBox">
<audio src={audioURL} controls />
<button onClick={startRecording} disabled={isRecording}>
start recording
</button>
<button onClick={() => {
stopRecording();
}} disabled={!isRecording}>
stop recording
</button>
<button onClick={() => {
submitNewMessage();
}}>
send recording
</button>
<p>
<em>
</em>
</p>
</div>
);
}
export default RecorderBox;
TL;DR: submitNewMessage is running in the scope of the previous render, thus attachFile doesn't contain anything yet. Whatever audio you managed to send on the second click was actually the first recording you tried to send.
Solution: Skip attachFile completely.
A couple of hints: Don't have a useEffect depend on a state that the effect itself is updating, and dont use a useEffect for doing something that a click-handler can do.
Here's a safer version of your code. I haven't tested it, but any bugs should be easy to fix.
import { useCallback, useEffect, useRef, useState } from 'react';
// This is a nugget I use in many similar cases.
/**
* Similar to `useMemo`, but accepts a callback that return a promise.
* Very useful when working with states that depend on some async function.
*
* #link https://gist.github.com/mariusbrataas/ffca05c210ec540b4fb9f4324c4f2e68
*
* #example
* function MyComponent({url}:{url: string}) {
* const data = usePromiseMemo(
* () =>
* fetch(url)
* .then(r => r.json())
* .then(r => r.my.user.info),
* [url]
* );
*
* return <p>{info || "Loading info..."}</p>
* }
*/
export function usePromiseMemo<T>(
callback: () => Promise<T> | T,
deps?: any[]
) {
// Private states
const [state, setState] = useState<T>();
const isMounted = useRef(true);
// Register unmount
useEffect(() => {
if (!isMounted.current) isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
// Execute callback
useEffect(() => {
if (isMounted.current) {
if (state !== undefined) setState(undefined);
Promise.resolve()
.then(() => callback())
.then(r => {
if (isMounted.current) setState(r);
});
}
}, deps || []);
// Return state
return state;
}
function useRecorder() {
const [audioURL, setAudioURL] = useState<string>();
const [audioBlob, setAudioBlob] = useState<File>();
const [isRecording, setIsRecording] = useState(false);
// Handler for recorded data
const handleRecorderData = useCallback(
(e: BlobEvent) => {
setAudioURL(URL.createObjectURL(e.data));
setAudioBlob(new File([e.data], 'incomingaudioclip.wav'));
},
[setAudioURL, setAudioBlob]
);
// Create recorder
const recorder = usePromiseMemo(
() =>
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(stream => new MediaRecorder(stream)),
[]
);
// Attach event listener
useEffect(() => {
if (recorder)
recorder.addEventListener('dataavailable', handleRecorderData);
return () => {
if (recorder)
recorder.removeEventListener('dataavailable', handleRecorderData);
};
}, [recorder, handleRecorderData]);
// Handle start recording
const startRecording = useCallback(() => {
if (recorder) {
recorder.start();
setIsRecording(true);
}
}, [recorder, setIsRecording]);
// Handle stop recording
const stopRecording = useCallback(() => {
if (recorder) {
recorder.stop();
setIsRecording(false);
}
}, [recorder, setIsRecording]);
// Return
return {
audioURL,
audioBlob,
setAudioBlob,
isRecording,
startRecording,
stopRecording
};
}
export const RecorderBox = ({
log,
handleUpdateLog,
handleSubmitNewMessage,
selectedLog
}: any) => {
const {
audioURL,
audioBlob,
setAudioBlob,
isRecording,
startRecording,
stopRecording
} = useRecorder();
const [newMessage, setNewMessage] = useState('');
// Create a decorated submit function
const decoratedSubmitNewMessage = useCallback(() => {
const body = {
body: newMessage,
attachment: audioBlob
};
handleUpdateLog(log, newMessage, audioBlob);
// handleSubmitNewMessage(log.id, body); // You had this commented out, but it should work just fine.
}, [handleUpdateLog, handleSubmitNewMessage, audioBlob, newMessage]);
// Cleanup whenever receiving new object for `log`
useEffect(() => {
setNewMessage('');
setAudioBlob(undefined);
document.getElementsByClassName('dzu-previewButton')[0]?.click(); // No idea what this is supposed to do.
}, [log, setNewMessage, setAudioBlob]);
return (
<div className="RecorderBox">
<audio src={audioURL} controls />
<button onClick={startRecording} disabled={isRecording}>
start recording
</button>
<button onClick={stopRecording} disabled={!isRecording}>
stop recording
</button>
<button onClick={decoratedSubmitNewMessage}>send recording</button>
<p>
<em></em>
</p>
</div>
);
};
JavaScript version:
import { useCallback, useEffect, useRef, useState } from 'react';
// This is a nugget I use in many similar cases.
/**
* Similar to `useMemo`, but accepts a callback that return a promise.
* Very useful when working with states that depend on some async function.
*
* #link https://gist.github.com/mariusbrataas/ffca05c210ec540b4fb9f4324c4f2e68
*
* #example
* function MyComponent({url}:{url: string}) {
* const data = usePromiseMemo(
* () =>
* fetch(url)
* .then(r => r.json())
* .then(r => r.my.user.info),
* [url]
* );
*
* return <p>{info || "Loading info..."}</p>
* }
*/
export function usePromiseMemo(callback, deps) {
// Private states
const [state, setState] = useState();
const isMounted = useRef(true);
// Register unmount
useEffect(() => {
if (!isMounted.current) isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
// Execute callback
useEffect(() => {
if (isMounted.current) {
if (state !== undefined) setState(undefined);
Promise.resolve()
.then(() => callback())
.then(r => {
if (isMounted.current) setState(r);
});
}
}, deps || []);
// Return state
return state;
}
function useRecorder() {
const [audioURL, setAudioURL] = useState();
const [audioBlob, setAudioBlob] = useState();
const [isRecording, setIsRecording] = useState(false);
// Handler for recorded data
const handleRecorderData = useCallback(
e => {
setAudioURL(URL.createObjectURL(e.data));
setAudioBlob(new File([e.data], 'incomingaudioclip.wav'));
},
[setAudioURL, setAudioBlob]
);
// Create recorder
const recorder = usePromiseMemo(
() =>
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(stream => new MediaRecorder(stream)),
[]
);
// Attach event listener
useEffect(() => {
if (recorder)
recorder.addEventListener('dataavailable', handleRecorderData);
return () => {
if (recorder)
recorder.removeEventListener('dataavailable', handleRecorderData);
};
}, [recorder, handleRecorderData]);
// Handle start recording
const startRecording = useCallback(() => {
if (recorder) {
recorder.start();
setIsRecording(true);
}
}, [recorder, setIsRecording]);
// Handle stop recording
const stopRecording = useCallback(() => {
if (recorder) {
recorder.stop();
setIsRecording(false);
}
}, [recorder, setIsRecording]);
// Return
return {
audioURL,
audioBlob,
setAudioBlob,
isRecording,
startRecording,
stopRecording
};
}
export const RecorderBox = ({
log,
handleUpdateLog,
handleSubmitNewMessage,
selectedLog
}) => {
const {
audioURL,
audioBlob,
setAudioBlob,
isRecording,
startRecording,
stopRecording
} = useRecorder();
const [newMessage, setNewMessage] = useState('');
// Create a decorated submit function
const decoratedSubmitNewMessage = useCallback(() => {
const body = {
body: newMessage,
attachment: audioBlob
};
handleUpdateLog(log, newMessage, audioBlob);
// handleSubmitNewMessage(log.id, body); // You had this commented out, but it should work just fine.
}, [handleUpdateLog, handleSubmitNewMessage, audioBlob, newMessage]);
// Cleanup whenever receiving new object for `log`
useEffect(() => {
setNewMessage('');
setAudioBlob(undefined);
document.getElementsByClassName('dzu-previewButton')[0]?.click(); // No idea what this is supposed to do.
}, [log, setNewMessage, setAudioBlob]);
return (
<div className="RecorderBox">
<audio src={audioURL} controls />
<button onClick={startRecording} disabled={isRecording}>
start recording
</button>
<button onClick={stopRecording} disabled={!isRecording}>
stop recording
</button>
<button onClick={decoratedSubmitNewMessage}>send recording</button>
<p>
<em></em>
</p>
</div>
);
};
That's the warning in the console,
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.
Here is my code
const [index, setIndex] = useState(0);
const [refreshing, setRefreshing] = useState(false);
const refContainer: any = useRef();
const [selectedIndex, setSelectedIndex] = useState(0);
const navigation = useNavigation();
useEffect(() => {
refContainer.current.scrollToIndex({animated: true, index});
}, [index]);
const theNext = (index: number) => {
if (index < departments.length - 1) {
setIndex(index + 1);
setSelectedIndex(index + 1);
}
};
setTimeout(() => {
theNext(index);
if (index === departments.length - 1) {
setIndex(0);
setSelectedIndex(0);
}
}, 4000);
const onRefresh = () => {
if (refreshing === false) {
setRefreshing(true);
setTimeout(() => {
setRefreshing(false);
}, 2000);
}
};
What should I do to make clean up?
I tried to do many things but the warning doesn't disappear
setTimeout need to use in useEffect instead. And add clear timeout in return
useEffect(() => {
const timeOut = setTimeout(() => {
theNext(index);
if (index === departments.length - 1) {
setIndex(0);
setSelectedIndex(0);
}
}, 4000);
return () => {
if (timeOut) {
clearTimeout(timeOut);
}
};
}, []);
Here is a simple solution. first of all, you have to remove all the timers like this.
useEffect(() => {
return () => remover timers here ;
},[])
and put this
import React, { useEffect,useRef, useState } from 'react'
const Example = () => {
const isScreenMounted = useRef(true)
useEffect(() => {
isScreenMounted.current = true
return () => isScreenMounted.current = false
},[])
const somefunction = () => {
// put this statement before every state update and you will never get that earrning
if(!isScreenMounted.current) return;
/// put here state update function
}
return null
}
export default Example;
There is a simple code where i make the api call with react redux.
But there is one more thing. periodically i increase the progress value I show on the screen. I use useEffect for this. but when i increase progress, api goes back to call. I just want to make my api call once.
here is an example of my code
const Do = () => {
const [progress, setProgress] = useState(1);
const dispatch = useDispatch();
dispatch(myApiCall);
useEffect(() => {
const interval = setInterval(() => {
setProgress(progress => progress + 10);
}, 1500);
return () => clearInterval(interval);
}, [progress]);
return (
<ProgressBar
completed={progress}
/>
);
};
You only need to call the API in useEffect too:
const Do = () => {
const [progress, setProgress] = useState(1);
const dispatch = useDispatch();
useEffect(() => {
dispatch(myApiCall);
}, [dispatch]);
useEffect(() => {
const interval = setInterval(() => {
setProgress((progress) => progress + 10);
}, 1500);
return () => clearInterval(interval);
}, []);
return <ProgressBar completed={progress} />;
};
You need to call your api call inside of a componentDidMount equivalent useEffect:
const Do = () => {
const [progress, setProgress] = useState(1);
const dispatch = useDispatch();
useEffect(() => {
dispatch(myApiCall);
}, []);
useEffect(() => {
const interval = setInterval(() => {
setProgress(progress => progress + 10);
}, 1500);
return () => clearInterval(interval);
}, [progress]);
return (
<ProgressBar
completed={progress}
/>
);
};
I have hook useInterval which download data every 10 seconds automaticaly, however I have also button which can manually download data in every moment. I'm struggling to restart interval timer when I click button. So basically if interval counts to 5, but I click button meantime, interval should restart and starts counting to 10 again before downloading data
const useInterval = (callback, delay) => {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
const tick = () => {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};
export default useInterval;
APP PART:
useInterval(() => {
getMessage();
}, 10000)
const getMessage = async () => {
setProcessing(true)
try {
const res = await fetch('url')
const response = await res.json();
setRecievedData(response)
}
catch (e) {
console.log(e)
}
finally {
setProcessing(false)
}
}
const getMessageManually = () => {
getMessage()
RESTART INTERVAL
}
You can add a reset function in the hook and return that function. The reset function should clear the existing interval and start a new one.
Here is the code for the hook which can be reset and stopped.
const useInterval = (callback, delay) => {
const savedCallback = useRef(callback);
const intervalRef = useRef(null);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay !== null) {
const id = setInterval(savedCallback.current, delay);
intervalRef.current = id;
return () => clearInterval(id);
}
}, [delay]);
useEffect(()=>{
// clear interval on when component gets removed to avoid memory leaks
return () => clearInterval(intervalRef.current);
},[])
const reset = useCallback(() => {
if(intervalRef.current!==null){
clearInterval(intervalRef.current);
intervalRef.current = setInterval(savedCallback.current,delay)
}
});
const stop = useCallback(() => {
if(intervalRef.current!==null){
clearInterval(intervalRef.current);
}
})
return {
reset,
stop
};
};
// usage
const {reset,stop} = useInterval(()=>{},10000);
reset();
stop();
You should add a reset function as returning a value from the hook.
I also fixed few issues and added an unmount handler:
// Usage
const resetInterval = useInterval(() => ..., DELAY);
resetInterval();
// Implementation
const useInterval = (callback, delay) => {
const savedCallbackRef = useRef(callback);
const intervalIdRef = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// handle tick
useEffect(() => {
const tick = () => {
savedCallback.current();
};
if (delay !== null) {
intervalIdRef.current = setInterval(tick, delay);
}
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, [delay]);
// handle unmount
useEffect(() => {
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, []);
const resetInterval = useCallback(() => {
clearInterval(intervalIdRef.current);
intervalIdRef.current = setInterval(savedCallback.current, delay)
}, [delay]);
return resetInterval;
};
Another solution is to remove the ref on the callback making the hook restart the count on every change to the callback
so updating the above solution
// Implementation
const useInterval = (callback, delay) => {
const intervalIdRef = useRef();
// handle tick
useEffect(() => {
const tick = () => {
callback();
};
if (delay !== null) {
intervalIdRef.current = setInterval(tick, delay);
}
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, [delay]);
// handle unmount
useEffect(() => {
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, []);
};
And then you can use it like this
const [counter, setCounter] = useState[0]
const onTimerFinish = useCallback(() => {
setCounter(counter + 1)
// setCounter will reset the interval
}, [counter])
useResetInterval(() => {
onTimerFinish()
}, 5000)