How to wait for DOM commit in react - javascript

I am working on a ReactJS app that uses Webassembly with Emscripten. I need to run a series of algorithms from Emscripten in my Js and I need to show the results as they come, not altogether at the end. Something like this:
Run the first algo
Show results on-screen form first algo
Run the second algo AFTER the results from the first algo are rendedred on the screen (so that the user can keep checking them)
Show results on screen from the second algo, and so on.
To achieve this I'm using "UseEffect" like this:
useEffect(() => {
(
async function AlgoHandler(){
if (methodsToRun.length <= 0) {
setGlobalState('loadingResults', false);
return;
}
const method = methodsToRun[0];
let paramsTypes = method[1].map((param) => param[0][2]);
let runAlgo = window.wasm.cwrap(method[0], 'string', paramsTypes);
let params = method[1].map(
(param) => document.getElementById(param[0][0]).value
);
let result = await runAlgo(...params);
setGlobalState('dataOutput', (prev) => [...prev, ...JSON.parse(result)]);
await sleep(100);
setGlobalState('methodsToRun', (prev) => prev.filter(m => m != method));
})();
}, [methodsToRun]);
The problem is that this code "kind of" works, this is because useEffect ensures rendering before my callback, but DOM commits don't always happen, and the user doesn't see any updates on the UI. I would like to make it wait for the DOM commit of the component, not just the rerender, this is a component life-cycle in react:
So, any ideas on how I can achieve this?

The DOM commits always happen (React guarantees that), but the browser may not have had a chance to paint those commits.
You could use the usual setTimeout(/*...*/, 0) trick:
useEffect(() => {
setTimeout(() => {
async function AlgoHandler(){
if (methodsToRun.length <= 0) {
setGlobalState('loadingResults', false);
return;
}
const method = methodsToRun[0];
let paramsTypes = method[1].map((param) => param[0][2]);
let runAlgo = window.wasm.cwrap(method[0], 'string', paramsTypes);
let params = method[1].map(
(param) => document.getElementById(param[0][0]).value
);
let result = await runAlgo(...params);
setGlobalState('dataOutput', (prev) => [...prev, ...JSON.parse(result)]);
await sleep(100);
setGlobalState('methodsToRun', (prev) => prev.filter(m => m != method));
})();
}, 0);
}, [methodsToRun]);
That's not a guarantee, though. If you want to be really, really sure, wait for a requestAnimationFrame callback before doing the setTimeout(/*...*/, 0) call:
useEffect(() => {
requestAnimationFrame(() => {
setTimeout(() => {
async function AlgoHandler(){
if (methodsToRun.length <= 0) {
setGlobalState('loadingResults', false);
return;
}
const method = methodsToRun[0];
let paramsTypes = method[1].map((param) => param[0][2]);
let runAlgo = window.wasm.cwrap(method[0], 'string', paramsTypes);
let params = method[1].map(
(param) => document.getElementById(param[0][0]).value
);
let result = await runAlgo(...params);
setGlobalState('dataOutput', (prev) => [...prev, ...JSON.parse(result)]);
await sleep(100);
setGlobalState('methodsToRun', (prev) => prev.filter(m => m != method));
})();
}, 0);
});
}, [methodsToRun]);
You know from the fact rAF called your callback that the browser is just about to paint, so the setTimeout should be sufficient to wait until after the painting is complete.
This is sufficiently gnarly and magic-esque that it's likely best to wrap it in a hook:
const useAfterPaintEffect = (callback, deps) => {
// (Explanation of how this works goes here)
useEffect(() => {
requestAnimationFrame(() => {
setTimeout(() => {
callback();
}, 0);
});
}, deps);
};
then
useAfterPaintEffect(() => {
async function AlgoHandler(){
if (methodsToRun.length <= 0) {
setGlobalState('loadingResults', false);
return;
}
const method = methodsToRun[0];
let paramsTypes = method[1].map((param) => param[0][2]);
let runAlgo = window.wasm.cwrap(method[0], 'string', paramsTypes);
let params = method[1].map(
(param) => document.getElementById(param[0][0]).value
);
let result = await runAlgo(...params);
setGlobalState('dataOutput', (prev) => [...prev, ...JSON.parse(result)]);
await sleep(100);
setGlobalState('methodsToRun', (prev) => prev.filter(m => m != method));
})();
}, [methodsToRun]);
Note that that hook doesn't allow for cleanup. To do that, you'd have to make your callback return another callback (to do the actual work) and an optional cleanup callback.
const useAfterPaintEffect = (callback, deps) => {
// (Explanation of how this works goes here)
const [run, cleanup] = callback();
useEffect(() => {
requestAnimationFrame(() => {
setTimeout(() => {
run();
}, 0);
});
return cleanup;
}, deps);
};
then
useAfterPaintEffect(() => {
return [
() => {
async function AlgoHandler(){
if (methodsToRun.length <= 0) {
setGlobalState('loadingResults', false);
return;
}
const method = methodsToRun[0];
let paramsTypes = method[1].map((param) => param[0][2]);
let runAlgo = window.wasm.cwrap(method[0], 'string', paramsTypes);
let params = method[1].map(
(param) => document.getElementById(param[0][0]).value
);
let result = await runAlgo(...params);
setGlobalState('dataOutput', (prev) => [...prev, ...JSON.parse(result)]);
await sleep(100);
setGlobalState('methodsToRun', (prev) => prev.filter(m => m != method));
})();
},
// This specific one doesn't need a cleanup callback
];
}, [methodsToRun]);

Related

I am facing problem chaining the async code in Javascript

I am trying to execute allCountryData and return a promise its working fine but after allCountryData is done executing I want to perform a operation on that returned data / or allCountryDataArray and store the highest values in arrayOfHighestCases
Note I can't chain the other login in allCountryData.
Please help let me know if you need any more details
export const allCountryDataArray = [];
export const arrayOfHighestCases = [];
const allCountryData = async () => {
sendHTTP()
.then((res) => {
return res.response;
})
.then((res) => {
allCountryDataArray.push(...res);
return allCountryDataArray;
});
return await allCountryDataArray;
// Highest Cases
};
The code is below is not working
const highestCasesData = async () => {
// const allCountryDataArrayy = await allCountryData();
// allCountryData()
// .then((data) => {
// console.log(arrayOfHighestCases[0]);
// })
// .then((res) => {
const np = new Promise((res, rej) => {
res(allCountryData());
});
return np.then((res) => {
console.log(res);
const arrayofHigh = allCountryDataArray.sort((a, b) => {
if (a.cases.total < b.cases.total) {
return 1;
} else if (a.cases.total > b.cases.total) {
return -1;
} else {
return 0;
}
});
console.log(arrayofHigh);
const slicedArray = arrayofHigh.slice(0, 6);
for (const eachHighCase of slicedArray) {
arrayOfHighestCases.push(eachHighCase);
}
console.log(arrayOfHighestCases);
return arrayOfHighestCases;
});
// });
};
highestCasesData();
Filling global arrays with async data is a way into timing conflicts. Bugs where the data ain't there, except when you look it is there and yet another question here on my SO about "Why can't my code access data? When I check in the console everything looks fine, but my code ain't working."
If you want to store something, store Promises of these arrays or memoize the functions.
const allCountryData = async () => {
const res = await sendHTTP();
return res.response;
};
const highestCasesData = async () => {
const allCountryDataArray = await allCountryData();
return allCountryDataArray
.slice() // make a copy, don't mutate the original array
.sort((a, b) => b.cases.total - a.cases.total) // sort it by total cases DESC
.slice(0, 6); // take the first 6 items with the highest total cases
}
This is working please let me know if I can make some more improvements
const allCountryData = async () => {
return sendHTTP()
.then((res) => {
return res.response;
})
.then((res) => {
allCountryDataArray.push(...res);
return allCountryDataArray;
});
// Highest Cases
};
const highestCasesData = async () => {
return allCountryData().then((res) => {
console.log(res);
const arrayofHigh = allCountryDataArray.sort((a, b) => {
if (a.cases.total < b.cases.total) {
return 1;
} else if (a.cases.total > b.cases.total) {
return -1;
} else {
return 0;
}
});
console.log(arrayofHigh);
const slicedArray = arrayofHigh.slice(0, 6);
for (const eachHighCase of slicedArray) {
arrayOfHighestCases.push(eachHighCase);
}
console.log(arrayOfHighestCases);
return arrayOfHighestCases;
});
};
highestCasesData();

javascript - cancel awaited function before it is called again

On incoming webrtc call, I open a modal to show user a message about media permissions and await this till the user presses OK button on the modal.
I do it like this:
async function myMessage(){
$('#id_my_message_modal').modal('show');
return new Promise(resolve =>
$('#id_my_message_button').on('click', () => {
$('#id_my_message_modal').modal('hide');
resolve();
}
)
);
}
then
await myMessage();
The issue I am facing now is if await myMessage(); is called again while the previous call has still not returned(i.e user hasn't pressed OK button). I want a way to cancel any previous await myMessage();, if exists, before it is called again.
Is there any way to do it?
The first approach (Live demo)- add every call of the async function to the queue, so you will get a sequence of dialogs with the result returning (close/accept/whatever).
// decorator from my other answer
function asyncBottleneck(fn, concurrency = 1) {
const queue = [];
let pending = 0;
return async (...args) => {
if (pending === concurrency) {
await new Promise((resolve) => queue.push(resolve));
}
pending++;
return fn(...args).then((value) => {
pending--;
queue.length && queue.shift()();
return value;
});
};
}
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const myMessage = asyncBottleneck(async function () {
const $modal = $("#id_my_message_modal");
$modal.modal("show");
const result = await new Promise((resolve) =>
$modal.on("click", "button", (e) => {
$modal.modal("hide");
$modal.off("click", "button");
resolve($(e.target).data("action"));
})
);
await delay(250);
return result;
});
The second approach (Live demo)- multiplexing the fn calls, when every await of the function will return the same result; Compare console output of the live demos.
function singleThread(fn) {
let promise = null;
return function (...args) {
return (
promise ||
(promise = fn(...args).finally(() => {
promise = null;
}))
);
};
}
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const myMessage = singleThread(async function () {
const $modal = $("#id_my_message_modal");
$modal.modal("show");
const result = await new Promise((resolve) =>
$modal.on("click", "button", (e) => {
$modal.modal("hide");
$modal.off("click", "button");
resolve($(e.target).data("action"));
})
);
await delay(250);
return result;
});
$("#btn-run").on("click", async () => {
myMessage().then((c) => console.log(`First result: ${c}`));
await myMessage().then((c) => console.log(`Second result: ${c}`));
myMessage().then((c) => console.log(`Third result: ${c}`));
});
The third way- closing the previous modal with its promise rejecting (Live Demo open console there to see the result).
import CPromise from "c-promise2";
const showModal = (() => {
let prev;
return (id, text = "") => {
prev && prev.cancel();
return (prev = new CPromise((resolve, reject, { onCancel }) => {
const $modal = $(id);
text && $modal.find(".modal-body").text(text);
$modal.modal("show");
const dispose = () => {
$modal.modal("hide");
$modal.off("click", "button");
};
$modal.on("click", "button", function (e) {
dispose();
resolve($(this).data("action"));
});
$modal.on("hidden.bs.modal", () => {
setTimeout(() => resolve("close"));
});
onCancel(dispose);
})).finally(() => (prev = null));
};
})();
$("#btn-run").on("click", async () => {
showModal("#id_my_message_modal", "First message").then(
(c) => console.log(`First modal result: ${c}`),
(e) => console.warn(`First modal fail: ${e}`)
);
showModal("#id_my_message_modal", "Second message").then(
(c) => console.log(`Second modal result: ${c}`),
(e) => console.warn(`Second modal fail: ${e}`)
);
const promise = showModal("#id_my_message_modal", "Third message")
.then(
(c) => console.log(`Third modal result: ${c}`),
(e) => console.warn(`Third modal fail: ${e}`)
)
.timeout(5000)
.then(() => {
return showModal(
"#id_my_message_modal2",
"Blue Pill or Red Pill?"
).then((v) => console.log(`Pill: ${v}`));
});
/*setTimeout(()=>{
promise.cancel(); you can cancel the modal from your code
}, 1000);*/
});

React script stop working after changing API call

I have a script which calls API from React and then triggers email notification function.
I was changing one part of it to call whole array of parameters instead of calling one parameter after another.
Here is part before change(working one). Console log shows correct response and I receive email notification as well.
const getApiData = () => {
const apiCall = (symbol) => {
return `https://min-api.cryptocompare.com/data/pricemulti?fsyms=${symbol}&tsyms=USD&api_key=API-KEY-HERE`
}
const MAX_CHARACKTERS = 300
let bucketArray = ['']
for (let i=0; i < assets.length - 1; i += 1) {
const symbol = `${bucketArray[bucketArray.length - 1]},${assets[i]}`
if (i === 0) {
bucketArray[0] = assets[i]
continue
}
if (symbol.length < MAX_CHARACKTERS) {
bucketArray[bucketArray.length - 1] = symbol
} else {
bucketArray[bucketArray.length] = assets[i]
}
}
const getData = () => {
Promise.all(
bucketArray.map(req => {
return axios(apiCall(req))
.then(({ data }) => data)
})
).then((data) => setDataApi(data))
}
getData()
};
Here is problematic one.
const getApiData = () => {
const getString = symbol =>
`https://min-api.cryptocompare.com/data/pricemulti?fsyms=${symbol}&tsyms=USD&api_key=API-KEY-HERE`;
function getAxious(id) {
const url = getString(id);
return axios.get(url);
}
const BUCKET_SIZE = 150;
const bucketArray = assets.reduce(
(arr, rec) => {
if (arr[arr.length - 1].length < BUCKET_SIZE) {
arr[arr.length - 1] = [...arr[arr.length - 1], rec];
return arr;
}
return [...arr, [rec]];
},
[[]]
);
bucketArray
.reduce((acc, rec) => {
return acc.then(results => {
return Promise.all(
rec.map(item =>
getAxious(item).then(({ data }) => {
return {
Symbol: item,
Open: data
};
})
)
).then(x => {
return [...x, ...results];
});
});
},
Promise.resolve([]))
.then(res => {
setDataApi(res);
});
};
Here in console I receive empty array - [] no errors showed, but email notification also stops from working.
I'm changing the code since I need to call whole array from API in one call. Before I was calling one symbol after another.
What I did wrong that console doesn't show the correct response?
EDIT1
Here is bucketArray value
const assets = ['ADA','KAVA','DOGE'];
I was not able to understand completely, but I think you want to collect all the results together and set it to the data using setDataApi.
Check the below code and let me know if it helps:
async function getApiData() {
const getString = (arr) =>
`https://min-api.cryptocompare.com/data/pricemulti?fsyms=${arr.join(
","
)}&tsyms=USD&api_key=API_KEY`;
function getAxious(arr) {
const url = getString(arr);
return axios.get(url);
}
const BUCKET_SIZE = 150;
const bucketArray = assets.reduce(
(arr, rec) => {
if (arr[arr.length - 1].length < BUCKET_SIZE) {
arr[arr.length - 1] = [...arr[arr.length - 1], rec];
return arr;
}
return [...arr, [rec]];
},
[[]]
);
const res = await getAxious(bucketArray);
console.log("res", res);
return res;
// after this you can set setDataApi(res);
}
// keep this useEffect sepearate
const [timer, setTimer] = useState(null);
useEffect(() => {
async function getApiDatahandler() {
const res = await getApiData();
console.log(res);
const timerId = setTimeout(() => {
getApiDatahandler();
}, 1000 * 60);
setTimer(timerId);
setDataApi(res)
// set the data setDataApi(res);
}
getApiDatahandler();
return () => {
window.clearTimeout(timer);
};
}, []);
// useEffect(() => {
// const timerId = setTimeout(() => {
// getApiData();
// }, 1000 * 60);
// }, [])
Checkout this codepen for a possible solution.
https://codepen.io/bcaure/pen/RwapqZW?editors=1011
In short, I don't know how to fix your code because it's quite a callback hell.
// Mock API and data
const bucketArray = [[{item: 'item1'}], [{item: 'item2'}], [{item: 'item3'}]];
const getAxious = item => {
return new Promise((resolve, reject) => resolve({data: 'API data'}));
}
// Return promise that combines API data + input item
const recToPromise = rec => rec.map(item => {
return new Promise((resolve, reject) => getAxious(item)
.then(data => resolve({item, data})));
});
// Flatten array
const recPromisesFlatten = bucketArray.flatMap(recToPromise);
Promise.all(recPromisesFlatten)
.then(res => {
const flattenRes = res.flatMap(({item, data}) => ({ Symbol: item, Open: data }));
console.log(JSON.Stringify(flattenRes))
});
What I'm suggesting to debug errors:
build your promise array first
then run Promise.all
then combine your data
Bonus: you can see flatMap instead of reduce for better readability.

Making parallel async await calls in ReactJS

Currently each of the value is set after setting the previous value, the async calls are not executed in parrallel. How do I make these calls execute in parallel?
const [index, setIndex] = useState(0);
const [roll, setRollNo] = useState(1);
const [sem, setSemester] = useState(1);
useEffect(() => {
getMyValue();
}, []);
const getMyValue = async () => {
try {
setIndex(JSON.parse(await AsyncStorage.getItem('#branch')) || 0);
setSemester(JSON.parse(await AsyncStorage.getItem('#sem')) || 1);
setRollNo(JSON.parse(await AsyncStorage.getItem('#roll')) || 1);
} catch (e) {
// console.log(e);
}
};
You can use Promise.all
const [index, semester, roll] = await Promise.all([
AsyncStorage.getItem('#branch'),
AsyncStorage.getItem('#sem'),
AsyncStorage.getItem('#roll')]);
setIndex(JSON.parse(index) || 0);
setSemester(JSON.parse(semester) || 1);
setRollNo(JSON.parse(roll) || 1);
Or if you like to turn such thing into mapping monstrosity as recommended in the answers there you go...
const params = ['#branch', '#sem', '#roll'];
const defaultValues = [0, 1, 1];
const [index, semester, roll] = await Promise.all(
params.map(AsyncStorage.getItem))
.then((values) => values.map((pr, index) => JSON.parse(pr) || defaultValues[index]));
setIndex(index);
setSemester(semester);
setRollNo(roll);
To execute several promises in parallel you need organize them as array and execute unsing Promise.all:
const [index, setIndex] = useState(0);
const [roll, setRollNo] = useState(1);
const [sem, setSemester] = useState(1);
useEffect(() => {
getMyValue();
}, []);
const getMyValue = async () => {
try {
const itemsArr = ['#branch', '#sem', '#roll']
const result = await Promise.all(promisesArr.map(item => AsyncStorage.getItem(item)))
setIndex(JSON.parse(result[0]) || 0);
setSemester(JSON.parse(result[1]) || 1);
setRollNo(JSON.parse(result[2]) || 1);
} catch (e) {
// console.log(e);
}
};
React JS does not batch the state updates if the event handler is async.
In your example, as you are await-ing on AsyncStorage.getItem, they are not batched.
You can #Józef 's solution to batch them up
Reference: https://github.com/facebook/react/issues/14259#issuecomment-450118131
Using for await of ...
We use an array of jobs to getItem, then after await, we can set the state of each item based on its type.
#Jozef's answer is also a great option as well, very good use of array deconstruction there
const response = []
const jobs = ["#branch", "#sem", "#roll"]
for await (item of jobs) {
const res = await AsyncStorage.getItem(item);
response.push({
type: item,
res
})
}
/**
response
[
{ type: "#branch", res: <res> },
{ type: "#sem", res: <res> },
{ type: "#roll", res: <res> }]
*/
response.forEach(item => {
const { type, res } = item;
if (type === "#branch") {
setIndex(JSON.parse(res))
} else if (type === "#semester") {
setSemester(JSON.parse(res))
} else if (type === "#roll") {
setRollNo(JSON.parse(res))
}
})
you don't need a async await here as you don't want to wait just use a promise instead
const getMyValue2 = () => {
try {
Promise.all([
Promise.resolve( AsyncStorage.getItem('#branch')),
Promise.resolve( AsyncStorage.getItem('#sem')),
Promise.resolve( AsyncStorage.getItem('#roll'))
]).then(data => {
setIndex(JSON.parse(data[0] || 0));
setRollNo(JSON.parse(data[1] || 1));
setSemester(JSON.parse(data[2] || 1));
});
} catch (e) {
// console.log(e);
}
};

Async/await in componentDidMount to load in correct order

I am having some troubles getting several functions loading in the correct order. From my code below, the first and second functions are to get the companyID companyReference and are not reliant on one and another.
The third function requires the state set by the first and second functions in order to perform the objective of getting the companyName.
async componentDidMount() {
const a = await this.companyIdParams();
const b = await this.getCompanyReference();
const c = await this.getCompanyName();
a;
b;
c;
}
componentWillUnmount() {
this.isCancelled = true;
}
companyIdParams = () => {
const urlString = location.href;
const company = urlString
.split('/')
.filter(Boolean)
.pop();
!this.isCancelled &&
this.setState({
companyID: company
});
};
getCompanyReference = () => {
const { firebase, authUser } = this.props;
const uid = authUser.uid;
const getUser = firebase.user(uid);
getUser.onSnapshot(doc => {
!this.isCancelled &&
this.setState({
companyReference: doc.data().companyReference
});
});
};
getCompanyName = () => {
const { firebase } = this.props;
const { companyID, companyReference } = this.state;
const cid = companyID;
if (companyReference.includes(cid)) {
const getCompany = firebase.company(cid);
getCompany.onSnapshot(doc => {
!this.isCancelled &&
this.setState({
companyName: doc.data().companyName,
loading: false
});
});
} else if (cid !== null && !companyReference.includes(cid)) {
navigate(ROUTES.INDEX);
}
};
How can I achieve this inside componentDidMount?
setState is asynchronous, so you can't determinate when the state is updated in a sync way.
1)
I recommend you don't use componentDidMount with async, because this method belongs to react lifecycle.
Instead you could do:
componentDidMount() {
this.fetchData();
}
fetchData = async () => {
const a = await this.companyIdParams();
const b = await this.getCompanyReference();
const c = await this.getCompanyName();
}
2)
The companyIdParams method doesn't have a return, so you are waiting for nothing.
If you need to wait I would return a promise when setState is finished;
companyIdParams = () => {
return new Promise(resolve => {
const urlString = location.href;
const company = urlString
.split('/')
.filter(Boolean)
.pop();
!this.isCancelled &&
this.setState({
companyID: company
}, () => { resolve() });
});
};
The same for getCompanyReference:
getCompanyReference = () => {
return new Promise(resolve => {
const { firebase, authUser } = this.props;
const uid = authUser.uid;
const getUser = firebase.user(uid);
getUser.onSnapshot(doc => {
!this.isCancelled &&
this.setState({
companyReference: doc.data().companyReference
}, () => { resolve() });
});
});
};
3)
If you want to parallelize the promises, you could change the previous code to this:
const [a, b] = await Promise.all([
await this.companyIdParams(),
await this.getCompanyReference()
]);
4)
According to your code, the third promise is not a promise, so you could update (again ;) the above code:
const [a, b] = .....
const c = this.getCompanyName()
EDIT: the bullet points aren't steps to follow
As the last api call is dependent on the response from the first 2 api calls, use a combination of Promise.all which when resolved will have the data to make the last dependent call
async componentDidMount() {
let [a, c] = await Promise.all([
this.companyIdParams(),
this.getCompanyReference()
]);
const c = await this.getCompanyName();
}

Categories