Canceling setTimeout early - javascript

I'm working on an audio recording class that either runs for an allotted period of time (such as 5 seconds) or can be stopped early by the user.
I'm using setTimeout to define the recording length, which works. However, I'm having trouble getting setTimeout working with a "stop" button. The error is as follows:
Cannot read properties of null (reading 'stop')
When the startRecording function executes, the handleStopRecording function is called which sets a timer with the "stopRecording" function. If the "stopRecording" function is called before the time elapses (by pressing the "stop" button), the function call that was initially in setTimeout will still execute when the timer expires, causing an error.
I tried fixing this by using clearTimeout, but then the "context" of the original function call is lost and we get the same error:
Cannot read properties of null (reading 'stop')
Unless I'm mistaken, I think this is an issue with closure of the setTimeout function - however I'm not sure how to clear the function early with a stop button and limit recording time.
Thank you in advance!
App.js (React.js)
import AudioRecorder from "./audioRecorder";
const App = () => {
const [recordedNameClipURL, setRecordedNameClipURL] = useState(null);
const [timeoutId, setTimeoutId] = useState(null);
const recorder = new AudioRecorder();
const startRecording = () => {
recorder.start();
handleStopRecording();
};
const handleStopRecording = async () => {
const id = setTimeout(stopRecording, 3000);
setTimeoutId(id);
};
const stopRecording = async () => {
clearTimeout(timeoutId);
const response = await recorder.stop();
setRecordedNameClipURL(response);
};
return (
...
);
};
audioRecorder.js
class AudioRecorder {
constructor() {
this.audioRecorder = null;
this.audioChunks = [];
}
initialize = async () => {
try {
await this.isSupported();
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.audioRecorder = new MediaRecorder(stream);
this.audioRecorder.addEventListener("dataavailable", event => {
this.audioChunks.push(event.data);
});
} catch (err) {
console.log(err.message);
}
};
start = async () => {
try {
await this.initialize();
this.audioRecorder.start();
} catch (err) {
console.log(err.message);
}
};
stop = async () => {
try {
this.audioRecorder.stop();
const blob = await this.stopStream();
return URL.createObjectURL(blob);
} catch (err) {
console.log(err.message);
}
};
stopStream = () => {
return new Promise(resolve => {
this.audioRecorder.addEventListener("stop", () => {
const audioBlob = new Blob(this.audioChunks, {
type: this.audioRecorder.mimeType,
});
resolve(audioBlob);
});
});
};
isSupported = () => {
return new Promise((resolve, reject) => {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
resolve(true);
}
reject(new Error("getUserMedia not supported on this browser!"));
});
};
}
export default AudioRecorder;

Store the timer inside a React Ref instead
I usually store timeout/interval IDs in a React ref, because storing the handle isn't really "application state" the way that other things are. Sometimes it's needed to avoid render thrashing.
Here's what that looks like:
let timerRef = React.useRef(null)
const handleStopRecording = async () => {
timerRef.current = setTimeout(stopRecording, 3000)
}
const stopRecording = async () => {
clearTimeout(timerRef.current)
timerRef.current = null // good idea to clean up your data
const response = await recorder.stop()
setRecordedNameClipURL(response)
}
Code that needs to know if the timer is running should consult the timerRef; no additional state is needed:
let timerIsRunning = !!timerRef.current

You can try using a boolean value to check if the process is stopped. You can store it in state and change its value when starting or stopping
const [isStopped, setIsStopped] = useState(false);
const handleStopRecording = async () => {
const id = setTimeout(() => {
if(!isStopped){
stopRecording
}
}, 3000);
setTimeoutId(id);
};

Related

Keep checking connection if available in Node and clear setTimeout

In node i keep checking if connection is available if it is it will get token and store in variable but if not then catch error will be thrown and it will then execute setTimeout to try again in 2 seconds.
But i think i need a clean way to clear the setTimeout because when i log it i keep getting higher number in Symbol(asyncId) this to me feels like it keeps adding setTimeout more and more to memory?
export const getToken = async () => {
try {
if (!GLOBAL_VARS.authToken) {
await auth(`Getting token`)
}
// Do more stuff
} catch (error: any) {
tokenTimeout()
}
}
getToken()
export const tokenTimeout = () => {
setTimeout(() => {
getToken()
}, 2000)
}
Yest it will add more setTimeouts to the memory.
you can clear it like this:
export const tokenTimeout = setTimeout(() => {
getToken()
}, 2000)
export const getToken = async () => {
try {
if (!GLOBAL_VARS.authToken) {
await auth(`Getting token`)
}
// Do more stuff
} catch (error: any) {
const timeout = tokenTimeout()
}
}
getToken()
/// use it wherever you want
clearTimeout(timeout);
Memory is going to increase because of whatever you are doing with auth() and not because you are creating timeouts.
The issue I see with your code is not really using async await properly. It should look more like
const sleep = (ms) =>
new Promise((resolve) => {
setTimeout(resolve, ms);
});
export const getToken = async (count = 0) => {
try {
if (!GLOBAL_VARS.authToken) {
await auth(`Getting token`);
}
} catch (error: any) {
await sleep(2000);
if (count < 10) await getToken(++count);
else throw new Error('Taking too long to get auth token');
}
}
(async function () {
await getToken();
})();

Awaiting status update from API with React Hooks (useEffect)?

I am trying to achieve the following:
Using React + React Hooks + useEffect
Make an API post request
Receiving an ID
Making another post request using the ID to get the status (Queued or Completed)
So basically
{
id: "xxxxxxx",
status: "queued"
}
Is what I get back. Now the processing time varies, but I want to periodically check if the status has changed from "queued" to "completed". And while it's not completed, I want to display a loading spinner.
What would be the best way to do this? Would this be possible with promises / async functions? Or do I have to use some kind of interval to re-check the status periodically?
I am basically trying to use this in React: https://docs.assemblyai.com/walkthroughs#authentication
You could do something like this:
// Mocks - after the 2nd attempt getStatus will return "done"
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let attempt = 0;
const getId = async () => {
console.log("getId");
await delay();
return { id: "id1" };
};
const getStatus = async (id) => {
console.log("getStatus", { id, attempt });
attempt += 1;
await delay(1000);
return {
id,
status: attempt < 2 ? "queued" : "done"
};
};
export default function App() {
const [id, setId] = useState();
const [isDone, setIsDone] = useState(false);
useEffect(() => {
const effect = async () => {
const { id } = await getId();
setId(id);
};
effect();
}, []);
useEffect(() => {
let cancelled = false;
const effect = async () => {
let { status } = await getStatus(id);
while (status !== "done") {
if (cancelled) {
return;
}
await delay(1000);
status = (await getStatus(id)).status;
}
setIsDone(true);
};
if (id) {
effect();
}
return () => {
cancelled = true;
};
}, [id]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>{isDone ? "Done" : "Loading..."}</h2>
</div>
);
}
Note: this is simplified and does not cover error scenarios, but shows how this can be put together

React customHook using useEffect with async

I have a customHook with using useEffect and I would like it to return a result once useEffect is done, however, it always return before my async method is done...
// customHook
const useLoadData = (startLoading, userId, hasError) => {
const [loadDone, setLoadDone] = useState(false);
const loadWebsite = async(userId) => {
await apiService.call(...);
console.log('service call is completed');
dispatch(someAction);
}
useEffect(() => {
// define async function inside useEffect
const loadData = async () => {
if (!hasError) {
await loadWebsite();
}
}
// call the above function based on flag
if (startLoading) {
await loadData();
setLoadDone(true);
} else {
setLoadDone(false);
}
}, [startLoading]);
return loadDone;
}
// main component
const mainComp = () => {
const [startLoad, setStartLoad] = useState(true);
const loadDone = useLoadData(startLoad, 1, false);
useEffect(() => {
console.log('in useEffect loadDone is: ', loadDone);
if (loadDone) {
// do something
setStartLoad(false); //avoid load twice
} else {
// do something
}
}, [startLoad, loadDone]);
useAnotherHook(loadDone); // this hook will use the result of my `useLoadData` hook as an execution flag and do something else, however, the `loadDone` always false as returning from my `useLoadData` hook
}
It seems in my useDataLoad hook, it does not wait until my async function loadData to be finished but return loadDone as false always, even that I have put await keyword to my loadData function, and setLoadDone(true) after that, it still returns false always, what would be wrong with my implementation here and how could I return the value correct through async method inside customHook?
Well...it seems to be working after I put the setLoadDone(true); inside my async method, not inside useEffect, although I am not sure why...
updated code:
// customHook
const useLoadData = (startLoading, userId, hasError) => {
const [loadDone, setLoadDone] = useState(false);
const loadWebsite = async(userId) => {
await apiService.call(...);
console.log('service call is completed');
dispatch(someAction);
setLoadDone(true);
}
useEffect(() => {
// define async function inside useEffect
const loadData = async () => {
if (!hasError) {
await loadWebsite();
}
}
// call the above function based on flag
if (startLoading) {
await loadData();
// setLoadDone(true); doesn't work here
}
}, [startLoading]);
return loadDone;
}

Why is the function never reached?

So this is probably quite simple but I can't find the solution or what is happening. I am trying to call the sendRecording from the event handler inside the handleRecorder function. But the sendRecording is never reached:
import React from 'react';
import axios from 'axios';
declare var MediaRecorder: any;
const sendRecording = () => async dispatch => {
console.log('3');
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({});
try {
const res = await axios.post('/api/recording', body, config);
} catch (err) {
const errors = err.response.data.errors;
}
};
const handleRecorder = () => {
console.log('1');
sendRecording();
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
const mediaRecorder = new MediaRecorder(stream);
mediaRecorder.start();
const audioChunks: any[] = [];
mediaRecorder.addEventListener('dataavailable', event => {
audioChunks.push(event.data);
});
mediaRecorder.addEventListener('stop', () => {
console.log('2');
sendRecording();
});
setTimeout(() => {
mediaRecorder.stop();
}, 3000);
});
};
const Recorder = () => {
return (
<div>
<button className='btn' onClick={handleRecorder}>
<i className='fa fa-microphone' title='Record' />
</button>
</div>
);
};
export default Recorder;
When I click the button, the console prints:
1
2
But never the 3
It actually happens the same when I call the sendRecording function from the beginning of the handleRecorder function.
I'm still learning, it must be something simple that I have not understood, but it is taking me ages to solve this.
What you have done was a closure function, which is a function inside a function
you can find out more about that here: https://javascript.info/closure.
The way you have defined the function sendRecording is incorrect for this usage.
It should be something like this:
const sendRecording = async (dispatch) => {
...
}
not sure why you have an argument called dispatch, especially since it's not used.
you can now execute this function by simply calling
sendRecording();
why you sending in the dispatch params?
try removing it to :
const sendRecording = async () => {
console.log('3');
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({});
try {
const res = await axios.post('/api/recording', body, config);
} catch (err) {
const errors = err.response.data.errors;
}
};
Thank you very much to all of you!
I don't know what I did there ^^"
The dispatch was there because I was using it but I removed some code before posting it here but forgot to remove the dispatch.
It works perfectly anyway, thank you.

trouble with a while loop

Im trying to use a while loop with my util() function (its commented out at the bottom of the code). When I try to run the program, I am stuck in an endless loop where i dont get farther than where I'm console logging out "getProjects running"
const axios = require("axios");
const _ = require("lodash");
axios.defaults.headers.common["Private-Token"] = "iTookMyPrivateKeyOut";
const user = "yshuman1";
let projectArray = [];
let reposExist = true;
async function getProjects() {
console.log("getProjects running");
await axios
.get(`https://gitlab.com/api/v4/users/${user}/projects`)
.then(function(response) {
const arr = _.map(response.data, "id").forEach(repo => {
projectArray.push(repo);
});
console.log(projectArray);
});
}
function deleteRepo(projectArray) {
console.log("array size", projectArray.length);
const arr = _.map(projectArray).forEach(item => {
axios
.delete(`https://gitlab.com/api/v4/projects/${item}`)
.then(() => {
console.log("deleted project ID: ", item);
})
.catch(error => {
console.log(error);
});
});
}
function util() {
getProjects()
.then(() => {
if (projectArray.length == 0) {
reposExist = false;
}
if (projectArray.length < 20) {
console.log("array is less than 20");
reposExist = false;
}
deleteRepo(projectArray);
})
.catch(error => {
console.log(error);
});
}
// while (reposExist) {
// util();
// }
The while loop is synchronous, while everything in any .then (or promise await) will be asynchronous. The initial thread will never terminate. Your code will simply queue up unlimited calls of getProjects which will only console.log.
The simple solution would be to figure out how often you want to call util (once a second? once every 5 seconds?) and await a Promise that resolves after that amount of time on each iteration.
let reposExist = true;
function util() {
console.log('running');
}
const resolveAfter5 = () => new Promise(res => setTimeout(res, 5000));
(async () => {
while (reposExist) {
util();
await resolveAfter5();
}
})();

Categories