Batching React updates across microtasks? - javascript

I have code that looks something like:
// File1
async function fetchData() {
const data = await fetch(...);
setState({ data });
return data;
}
// File2
useEffect(() => {
(async () => {
const data = await fetchData();
setState({ data });
})();
});
This triggers 2 React commits in 1 task. This makes my app less than 60FPS. Ideally, I'd like to batch the 2 setStates. Currently, it looks like this:
Pink represents React commits (DOM operations). The browser doesn't have a chance to repaint until the second commit is done. I can give the browser a chance to repaint by adding await new Promise(succ => setTimeout(succ, 0)); between the setStates, but it'll be better if I could batch the commits.
It's also pretty much impossible to refactor this, since the useState exists in separate files.
I tried unstable_batchedUpdates but it doesn't work with async.

You can group fetchData, when fetchData is called with the same argument the cache is checked for a promise and that promise is returned instead of creating a new one (make a new fetch).
When the promise resolves then that cache entry is removed so when component mounts again it will fetch again. To change this behaviour you can pass a different cache object to the group funciton.
//group function (will always return promise)
const createGroup = (cache) => (
fn,
getKey = (...x) => JSON.stringify(x)
) => (...args) => {
const key = getKey(args);
let result = cache.get(key);
if (result) {
return result;
}
//no cache
result = Promise.resolve(fn.apply(null, args)).then(
(r) => {
cache.resolved(key); //tell cache promise is done
return r;
},
(e) => {
cache.resolve(key); //tell cache promise is done
return Promise.reject(e);
}
);
cache.set(key, result);
return result;
};
//cache that removes cache entry after resolve
const createCache = (cache = new Map()) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
//remove cache key when resolved
resolved: (key) => cache.delete(key),
//to keep cache:
//resolved: () => 'NO_OP',
};
};
//fetch data function
const fetchData = (...args) => {
console.log('fetch data called with', args);
return new Promise((resolve) =>
setTimeout(() => resolve(args), 1000)
);
};
//grouped fetch data
const groupedFetchData = createGroup(createCache())(
fetchData
);
groupedFetchData(1, 2, 3).then((resolve) =>
console.log('resolved with:', resolve)
);
groupedFetchData(1, 2, 3).then((resolve) =>
console.log('resolved with:', resolve)
);

I think you should be able to so something along the lines of this, the aim being to cache the calls for a certain amount of time and then pass them all to unstable_batchedUpdates at once.
import { unstable_batchedUpdates } from 'reactDOM'
import raf from 'raf'
const cache = []
let rafId = null
function setBatchedState(setState, data) {
cache.push({ setState, data })
if(!rafId) {
rafId = raf(() => {
unstable_batchedUpdates(() => {
cache.forEach(({setState, data}) => setState(data))
})
rafId = null
cache = []
})
}
}
export default setBatchedState
This is using requestAnimationFrame to debounce the calls to unstable_batchedUpdates, you may prefer to use setTimeout depending on your use case.

Related

How to concat multiple responses and set all response in an array [React JS]

I am doing an API call which is returning IDs and based on number of ids I am doing another call and trying to combine the responses but I am stuck with async issues.
const SearchUser = async () => {
try {
const response = await getSearchUsers();
const ids = response.data?.map((user) => user.userId);
await ids.forEach(async (id) => {
const result = await getUserInfo(id);
setRNOUsers(...result);
// combine result in one state
});
} catch (error) {
setSearching(false);
}
};
useEffect(() => {
SearchUser();
console.log('RNOUsers', RNOUsers); // this is empty and runs even before callng api
}, []);
How can handle this?
You can use Promise.all to wait for all responses, and then set them together with setRNOUsers
const SearchUser = async () => {
try {
const response = await getSearchUsers();
const ids = response.data?.map((user) => user.userId);
const responses = await Promise.all(ids.map(id => getUserInfo(id)))
setRNOUsers(...responses.flatMap(x => x));
} catch (error) {
setSearching(false);
}
};
useEffect(() => {
SearchUser();
console.log('RNOUsers', RNOUsers);
}, []);
Side note, the problem with console.log('RNOUsers', RNOUsers) is setRNOUsers (initialized by useState) is asynchronous. Besides that, your API calls are also asynchronous, so you cannot get values from RNOUsers immediately in useEffect. If you want to see data in that log, you should wait until the state is updated and your component gets re-rendered with your latest data.

`.pipe()` not executing `debounceTime`

I'm trying to debounce() an Observable with pipe() and chaining .subscribe() but for some reason the function in the subscribe is still being called over a dozen times in one go.
What I'm trying to do is pipe the withChangesForTables and debounce the sync call because I want it to be called only when a whole batch of changes have been made. So I created a provider for the sync and wrapped it around my RootNavigator
withChangesForTables on WatermelonDB source code
const SyncContext = createContext();
function useSync() {
return useContext(SyncContext);
}
function SyncProvider({children}) {
const [isSyncing, setIsSyncing] = useState(false);
const [hasUnsynced, setHasUnsynced] = useState(false);
async function checkUnsyncedChanges() {
const hasChanges = await hasUnsyncedChanges({
database
});
setHasUnsynced(hasChanges);
}
async function sync() {
await checkUnsyncedChanges();
if (!isSyncing && hasUnsynced) {
setIsSyncing(true);
await synchronizeWithServer();
setIsSyncing(false);
}
}
database.withChangesForTables([
'table_name',
'table_name2'
]).pipe(
skip(1),
// ignore records simply becoming `synced`
filter(changes => !changes.every(change => change.record.syncStatus === 'synced')),
// debounce to avoid syncing in the middle of related actions - I put 100000 to test only
debounceTime(100000),
).subscribe({
//calls API endpoint to sync local DB with server
next: () => sync(),
error: e => console.log(e)
});
const value = {
isSyncing,
hasUnsynced,
checkUnsyncedChanges,
sync
};
return (
<SyncContext.Provider value={value}>
{children}
</SyncContext.Provider>
);
}
I had to move withChangesForTables into a useEffect and retrun it in order to unsubcribe which seems to have resolved the issue. The code now looks something like this:
useEffect(() => {
return database.withChangesForTables([
'table_name',
'table_name2'
]).pipe(
skip(1),
filter(changes => !changes.every(change => change.record.syncStatus === 'synced')),
debounceTime(500),
).subscribe({
next: () => sync(),
error: e => console.log(e)
});
}, [])

Handle concurrent async updates to local state

I have a series of asynchronous calls that read from a local state S, perform some computation based on its current value, and return a new, update value of the local state S'
All this happens at runtime, so I have very little control over the order of these operations. This is a simplified version of what I have.
type State = {
state: number
}
let localState: State = {
state: 1000
}
const promiseTimeout = (time: number, value: number) => () => new Promise(
(resolve: (n: number) => void) => setTimeout(resolve, time, value + time)
);
const post: (n: number, currentState: State) => Promise<void> = (n, c) => promiseTimeout(n, c.state)()
.then(res => {
localState.state = res
console.log(localState)
})
post(1000, localState); // localState at call time is 1000
post(3000, localState); // localState at call time is still 1000
// when both promises resolve, the final value of localState will be 4000 instead of 5000
Playground link
This model is clearly broken, as both calls to post will read the same value of localState, while instead they should be performed sequentially.
If all calls were already determined at compile time, I could simply have something like
post(1000, localState)
.then(() => post(3000, localState)) // localState at call time is now 2000
How would I go about solving this?
One approach is to have post hook into a promise rather than working directly on the state object. That promise could be stored in the state object itself. It starts out fulfilled with the state object. post updates it like this:
const post = (n, state) => {
return state.promise = state.promise
.then(state => {
// ...do stuff here that updates (or replaces) `state`...
return state;
}));
};
Here's an example (in JavaScript, but you can add back the type annotations) using asyncAction (it's like your promiseTimeout, but without making it return a function we call immediately; not
"use strict";
let localState = {
state: 1000
};
localState.promise = Promise.resolve(localState);
// I'm not sure why this *returns* a function that we
// have to call, but...
const promiseTimeout = (time, value) => () => new Promise((resolve) => setTimeout(resolve, time, value + time));
const post = (n, state) => {
return state.promise = state.promise
.then(state => promiseTimeout(n, state.state)().then(newValue => {
state.state = newValue;
console.log(state.state);
return state;
}));
};
console.log("Running...");
post(1000, localState); // localState at call time is 1000
post(3000, localState); // localState at call time is still 1000
Since each call to post synchronously replaces the promise with a new promise, the chain is built by the calls to post.
Here's that in TypeScript (with a bit of a hack in one place, you can probably improve that); link to the playground.
type State = {
state: number,
promise: Promise<State>
};
let localState: State = (() => {
const s: Partial<State> = {
state: 1000
};
// There's probably a better way to handle this than type assertions, but...
s.promise = Promise.resolve(s as State);
return s as State;
})();
// I'm not sure why this *returns* a function that we
// have to call, but...
const promiseTimeout = (time: number, value: number) => () => new Promise(
(resolve: (n: number) => void) => setTimeout(resolve, time, value + time)
);
const post = (n: number, state: State): Promise<State> => {
return state.promise = state.promise
.then(state => promiseTimeout(n, state.state)().then(newValue => {
state.state = newValue;
console.log(state.state);
return state;
}));
};
console.log("Running...");
post(1000, localState); // localState at call time is 1000
post(3000, localState); // localState at call time is still 1000
It's worth noting that in situations like this where the state can be changed asynchronously like this, it's often worth producing a new state object when changing it rather than modifying the existing one — e.g., treat the state aspects as immutable.
This is a problem that I have personally encountered on many occasions. My solution is to create a queue class in charge of making sure that all Promise are executed in mutual exclusion. I call it PromiseQueue:
class PromiseQueue {
constructor() {
this._queue = new Array(); // Or an LinkedList for better performance
this._usingQueue = false;
}
/**
* Adds an element to the queue and runs the queue. It resolves when the promise has been executed and resolved.
*
* #param {Promise<any>} promise
*/
add(promise) {
const self = this;
return new Promise((resolve, reject) => {
const promiseData = {
promise,
resolve,
reject,
};
self._queue.push(promiseData);
self._runQueue();
});
}
async _runQueue() {
if (!this._usingQueue && this._queue.length > 0) {
this._usingQueue = true;
const nextPromiseData = this._queue.shift();
const { promise, resolve, reject } = nextPromiseData;
try {
const result = await promise();
resolve(result);
} catch (e) {
reject(e);
}
this._usingQueue = false;
this._runQueue();
}
}
}
Then you would use it like this (not tested):
const myPromiseQueue = new PromiseQueue();
// This way you are making sure that the second post
// will be executed when the first one has finished
myPromiseQueue.add(async() => await post(1000, localState));
myPromiseQueue.add(async() => await post(3000, localState));
I have no experience with TypeScript, so you'll have to do the conversion yourself.
You could considder adding a queue method to your state, which takes a callback. If the callback returns an promise it will wait for it to finish. If not the next item in the queue is immediately executed.
function createQueue() {
var promise = Promise.resolve();
return function (fn) {
promise = promise.then(() => fn(this));
return promise;
};
}
const localState = { state: 1000, queue: createQueue() };
const timeout = (...args) => new Promise(resolve => setTimeout(resolve, ...args));
const promiseTimeout = (time, value) => timeout(time, value + time);
const post = (time, state) => state.queue(() => {
return promiseTimeout(time, state.state).then(result => {
state.state = result;
console.log(state.state);
});
});
post(1000, localState).then(() => console.log("post 1000 complete"));
post(3000, localState).then(() => console.log("post 3000 complete"));

React state in render is unavailable inside return

I have these methods that do some fetching, and then once done, they set the state. But the render is called before the state is done and does not update.
The below seems to work on it's own, but takes a minute to finish.
//returns an promise with Array
getTopIDs(url) {
return fetch(url).then(blob => blob.json()).then(json => json)
}
// makes a URL fetchs JSON and return promise with single ID
getStory(id) {
let url = `https://hacker-news.firebaseio.com/v0/item/${id}.json?print=pretty`
return fetch(url).then(blob => blob.json()).then(json => json)
}
// call above methods, set state when done
componentDidMount() { //
let arr = []
let promise = new Promise((resolve, reject) => {
let data = this.getTopIDs("https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty").then((idArr) => {
idArr.forEach((id, index) => {
this.getStory(id).then(res => {
arr.push(res)
})
})
//resolve once all pushed to arr
resolve(arr)
})
})
// set state once array is completed
promise.then(res => {
return this.setState({data: arr})
})
}
Then in the render below it logs 'no', 'no' and stops. Trying it outside the return it logs 'no','yes'. Searching other posts for this I tried setting a boolean when done and using the state callback but those did not work (full disclosure: I don't really understand the setState callback option)
render() {
return (
<div>
{
this.state.data.length
? console.log('yes')
: console.log('no')
}
</div>)
}
I need render to handle this.state.data only when done. How can I do it?
Add fiddle: https://jsfiddle.net/drumgod/e2atysu3/6/
Your method this.getStory() is async but your handling of the array creation is sync inside your promise.
You need to either use async/await or only run your resolve(arr) after idArr.forEach() is for sure completed (which may be easier to do using Promise.all(idArr.map(...)) where the ... is returning the result from this.getStory()).
This is how you'll want to set your state inside getStory:
this.setState(prevState => ({
data: [...prevState.data, res]
}))
As mentioned in the comments, this would render the component for each data point in the forEach.
In order to avoid this issue, this is how componentDidMount() should be formatted:
componentDidMount() {
const arr = [];
this.getTopIDs("https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty").then((idArr) => {
idArr.forEach((id, index) => this.getStory(id).then(res => arr.push(res)));
this.setState(prevState => ({ data: [...prevState.data, arr] }))
})
}
This also lets you get rid of the promise.then call at the end.

How to make Observable.flatMap to wait to resolve

Playing with RxJS and React, I'm having problem of how to wait for data in Observable.fromPromise generated within map on another Observable.
I have async helper:
const dataStreamGenerator = (url = CLIENTS_DATA_URL) => (
Rx.Observable.fromPromise(fetch(url))
.flatMap(response => Rx.Observable.fromPromise(response.json()))
.catch(err => Rx.Observable.of(new Error(err)))
);
Then I have actions.fetchClients$ which is Rx.Subject:
actions.fetchClients$.map((url = CLIENTS_DATA_URL) => {
const ts = Date.now();
console.log('FETCH CLIENTS with: ', url);
return dataStreamGenerator(url);
}).map(val => {
console.log('GOT DATA IN REDUCER: ', val);
const error = (val instanceof Error) ? val.message : undefined;
const data = error ? undefined : val;
actions.receivedClientsData$.next({ data, error, ts: Date.now() });
return (state) => state;
})
(Yes, trying to mimick Redux in RxJS).
Whan I test the dataStreamGenerator, it works ok (with ava) and delivers data:
test('dataStreamGenerator', t => {
const ds$ = dataStreamGenerator(CLIENTS_DATA_URL);
return ds$.do((data) => {
t.is(data.length, 10);
});
});
(AVA automatically subscribe to observable and consume it, so there is no need to subscribe).
But the actions.fetchClients$.map((url = CLI... second map (starting... console.log('GOT DATA IN REDUCER: ', val);... is still getting the Observable and not the data from dataStream$.
I tried all possible combinations of map and flatMap in fetchClients$ code but still no luck.
My test code is:
test.only('fetchClients$', t => {
const initialState = {};
actions.fetchClients$.next('http://jsonplaceholder.typicode.com/users');
reducer$.subscribe(val => {
console.log('TEST SUBSCRIBE VAL: ', val);
t.pass();
});
actions.fetchClients$.next('http://jsonplaceholder.typicode.com/users');
});
I cant figure out how to wait to the Observable dataStreamGenerator(url); to emit data and not the Observable.
Thanks.
You need to flatten the results of what you returned from dataStreamGenerator.
actions.fetchClients$
//This is now a flatMap
.flatMap((url = CLIENTS_DATA_URL) => {
const ts = Date.now();
console.log('FETCH CLIENTS with: ', url);
return dataStreamGenerator(url);
});
The map operator delivers the value as-is down stream, whereas flatMap will flatten Observables, Arrays and Promises such that their values are what get propagated.
It works in the test case because you are directly subscribing to the Observable returned by dataStreamGenerator.

Categories

Resources