How can I make a function wait for another function? - reactjs

I have this function:
const submitImport = async (value) => {
const { actions, navigation, account } = this.props;
this.setState({ isImporting: true });
actions.importWallet(value.mnemonicPhrase, value.password, 1);
console.log('acc', account);
actions.showNotification({
message: 'Account has been successfully Imported',
isError: false,
});
};
importWallet is adding new properties to account object but when I call this function the first time the account object is empty but when I click it the second time it is okay. So my guess is importWallet needs time to finish and return the value. I tried to use async await but it did not work. Also, I tried to use return new Promise but it still did not work. Maybe I did it the wrong way idk.
Any suggestions on how to solve this issue please?

I am assuming the importWallet function induces some change in the account prop.
All prop changes require atleast one render cycle for the updated values to get visible as state/prop changes are asynchronous in react. In your case you are trying to access account as soon invoking actions.importWallet. Hence as it is within the same render, it has not yet been updated. But as you mentioned, it will be available from the subsequent renders.
Also you cannot make it synchronous with async-await / promises as the asynchronous nature of react state updates is not exposed.
Your usecase may be achieved by some refractor :
Try obtaining the new value of account in the return statement of account.importWallet. That way you can use it even before the prop updates.
const updatedAccount = actions.importWallet(value.mnemonicPhrase, value.password, 1);
In case you are using React Hooks, you can create an effect with useEffect and add account as a dependency. This will invoke the effect when the dependency value changes (dependency should be a primitive data type for consistent effect invocation).

Related

Excessive number of pending callbacks

I'm trying to arrange the data gotten from firebase but after I arrange it the app becomes slow and if I click on a button it gives an error saying "Excessive number of pending callbacks".
useEffect(() => {
if (chats.length && users.length) {
const list = [];
chats.forEach((chat) => {
if (chat.members.includes(userId)) {
chat.members.forEach((y) => {
if (y !== userId) {
console.log("receiver: " + y);
users.forEach((z) => {
if (z.id === y) {
console.log(z);
list.push({
chat: chat,
acc: z,
user: user
});
}
});
console.log(list);
}
});
}
});
setUserChats(list);
}
}, [chats, users]);
users and chats are both states that I got from firebase on snapshot also in useEffect
One guess: Your dependencies don't work in the way you expect them to. chats and users aren't primitives, so depending on how they get created and passed, it's possible useEffect is run on every render, no matter whether chat and users have changed in structure or not. So this is what might happen:
useEffect() will be called on every rerender due to your invalid dependencies.
useEffect() calls setUserChats() on every turn. In theory, setUserChats() will check whether the new state actually differs, but in the same manner as useEffect it "fails" in the comparison and takes every list as a new state.
setState() will trigger a new render. Rinse, repeat with 1)
What you need to understand it that useEffect checks whether dependencies have changed (and setUserChats() does so as well to decide whether new state actually differs from the old one). This check relies on the identity/equal reference, i.e. on oldValue === newValue. For non-primitive values that means: it doesn't matter if oldValue and newValue look alike or are perfect clones even - if they don't share the same address in the memory, they are taken as non-equal.
Check out this thread or this library for solutions. In your case, a simple (but not really nice) solution would be to change your dependencies to [JSON.stringify(chats), JSON.stringify(users)], but there are more elaborate, performant and reliable solutions out there.
(Additionally, you forget to add userId to the dependencies. So something like [userId, JSON.stringify(chats), JSON.stringify(users)] might be more appropriate.)
Another thought though: I don't see why all that logic requires to be put into a useEffect() anyway. Just calculate the list in the component itself and call setUserChats(list). Or does it take too long?

UseRef in Suspense data fetching

I'm trying to use the experimental new React feature Suspense for data fetching.
Here's my simple useApi hook which (if I understand Suspense correctly) either returns the result of an fetch call or throws the suspender promise. (slightly modified the documented example)
function useApi(path) {
const ref = React.useRef({ time: +new Date() });
if (!ref.current.suspender) {
ref.current.suspender = fetch(path).then(
data => ref.current.data = data,
error => ref.current.error = error,
);
}
if (ref.current.data) return ref.current.data;
if (ref.current.error) return ref.current.error;
throw ref.current.suspender;
}
I'm using this hook simply like this:
function Child({ path }) {
const data = useApi(path);
return "ok";
}
export default function App() {
return (
<Suspense fallback="Loading…">
<Child path="/some-path" />
</Suspense>
);
}
It never resolves.
I think the problem is that useRef isn't quite working as it's supposed to.
If I initialize the ref with a random value, it doesn't retain that value, and instead gets reinitialized with another random value:
const ref = React.useRef({ time: +new Date() });
console.log(ref.current.time)
1602067347386
1602067348447
1602067349822
1602067350895
...
There's something weird about throwing the suspender that causes the useRef to reinitialize on every call.
throw ref.current.suspender;
If I remove that line useRef works as intended, but obviously Suspense doesn't work.
Another way I can make it work is if I use some sort of custom caching outside of React, like:
const globalCache = {}
function useApi(path) {
const cached = globalCache[path] || (globalCache[path] = {});
if (!cached.suspender) {
cached.suspender = ...
}
if (cached.data) ...;
if (cached.error) ...;
throw cached.suspender;
}
This also makes it work, but I would rather use something that React itself provides in terms of caching component-specific data.
Am I missing something on how useRef is supposed to, or not supposed to work with Suspense?
Repro: https://codesandbox.io/s/falling-paper-shps2
Let's review some facts on React.Suspense:
The children elements of React.Suspense won't mount until the thrown promise resolved.
You must throw the promise from function body (not from a callback like useEffect).
Now, you throwing a promise from your custom hook, but according to 1. the component never mounts, so when the promised resolves, you throwing the promise again - infinite loop.
According to 2., even if you try saving the promise in a state or ref etc. still it wont work - infinite loop.
Therefore, if you want to write some custom hook, you indeed need to use any data-structure (can be managed globally {like your globalCache} or by React.Suspense parent) which indicates if the promise from this specific React.Suspense has been thrown (thats exactly what Relay does in Facebook's codebase).
I've been struggling with the same problem, but I think it's actually possible to achieve what you want. I looked at the implementations of react-async and SWR and noticed that react-async actually doesn't throw on the first render, but it uses useEffect(...) to start the async operation, combined with a setState which triggers another render and then throws the promise on subsequent renders (until it resolves). I believe SWR actually behaves the same, with one minor difference; SWR uses useLayoutEffect (with fallback to useEffect for server side rendering), which has one major benefit: the initial render without data never happens.
It does mean that the parent component still has to cope with abundance of data. The first render can be used to start the promise, but still has to return without throwing to avoid the infinite loop. Only on second render will the promise be thrown which will actually suspend rendering.

useState React hook always returning initial value

locationHistory is always an empty array in the following code:
export function LocationHistoryProvider({ history, children }) {
const [locationHistory, setLocationHistory] = useState([])
useEffect(() => history.listen((location, action) => {
console.log('old state:', locationHistory)
const newLocationHistory = locationHistory ? [...locationHistory, location.pathname] : [location.pathname]
setLocationHistory(newLocationHistory)
}), [history])
return <LocationHistoryContext.Provider value={locationHistory}>{children}</LocationHistoryContext.Provider>
}
console.log always logs []. I have tried doing exactly the same thing in a regular react class and it works fine, which leads me to think I am using hooks wrong.
Any advice would be much appreciated.
UPDATE: Removing the second argument to useEffect ([history]) fixes it. But why? The intention is that this effect will not need to be rerun on every rerender. Becuase it shouldn't need to be. I thought that was the way effects worked.
Adding an empty array also breaks it. It seems [locationHistory] must be added as the 2nd argument to useEffect which stops it from breaking (or no 2nd argument at all). But I am confused why this stops it from breaking? history.listen should run any time the location changes. Why does useEffect need to run again every time locationHistory changes, in order to avoid the aforementioned problem?
P.S. Play around with it here: https://codesandbox.io/s/react-router-ur4d3?fontsize=14 (thanks to lissitz for doing most the leg work there)
You're setting up a listener for the history object, right?
Assuming your history object will remain the same (the very same object reference) across multiple render, this is want you should do:
Set up the listener, after 1st render (i.e: after mounting)
Remove the listener, after unmount
For this you could do it like this:
useEffect(()=>{
history.listen(()=>{//DO WHATEVER});
return () => history.unsubscribe(); // PSEUDO CODE. YOU CAN RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[]); // THIS EMPTY ARRAY MAKES SURE YOUR EFFECT WILL ONLY RUN AFTER 1ST RENDER
But if your history object will change on every render, you'll need to:
cancel the last listener (from the previous render) and
set up a new listener every time your history object changes.
useEffect(()=>{
history.listen(()=>{//DO SOMETHING});
return () => history.unsubscribe(); // PSEUDO CODE. IN THIS CASE, YOU SHOULD RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[history]); // THIS ARRAY MAKES SURE YOUR EFFECT WILL RUN AFTER EVERY RENDER WITH A DIFFERENT `history` OBJECT
NOTE: setState functions are guaranteed to be the same instance across every render. So they don't need to be in the dependency array.
But if you want to access the current state inside of your useEffect. You shouldn't use it directly like you did with the locationHistory (you can, but if you do, you'll need to add it to the dependency array and your effect will run every time it changes). To avoid accessing it directly and adding it to the dependency array, you can do it like this, by using the functional form of the setState method.
setLocationHistory((prevState) => {
if (prevState.length > 0) {
// DO WHATEVER
}
return SOMETHING; // I.E.: SOMETHING WILL BE YOUR NEW STATE
});

How do I chain state updates?

I have a form with multiple controls that saves everything to a variable. Each control has an onChanged function, which runs a state update with that control's new value:
function onChangedValUpdate(newVal){
let fields = clone(this.state.fields);
fields[controlId] = newVal;
this.setState({fields});
}
My controls are dynamically created, and when they are, they run onChangedValUpdate on their initial value, if one is present. The problem is, sometimes a lot of controls are created at once, and React queues up its setStates using the same cloned fields object for each update. The object is not updated between setStates, presumably for similar reasons to this question. This means that, effectively, all but one control's updates are overwritten.
I tried writing an over-smart routine which used setState's callback to only run it if there isn't one already in progress and remember changes made to the fields variable in between setStates, but React went and ran all my queued updates simultaneously. Regardless, the routine felt too contrived to be right.
I'm sure this is a trivial and solved problem, but I can't seem to formulate my question in a Googleable way. How do I chain state updates that happen concurrently, and of which there may be any number?
EDIT For posterity, my solution, thanks to Yuri:
function onChangedValUpdate(newVal){
this.setState( state => {
let fields = clone(state.fields);
fields[controlId] = newVal;
return {fields};
}
}
You could pass a mutation function to setState. This will prevent overwritting on batched updates because every callback will get the most recent previous state.
function onChangedValUpdate(newVal){
this.setState(function(state){
const fields = clone(state.fields)
fields[controlId] = newVal
return {fields: fields}
});
}
Or using object spread and enhanced object literals.
function onChangedValUpdate(newVal){
this.setState(({fields}) => ({fields: {...fields, [controlId]: newVal}}));
}

Mobx: change states in action without runInAction still works

In mobx documentation:
action only affects the currently running function, not functions that are scheduled (but not invoked) by the current function! This means that if you have a setTimeout, promise.then or async construction, and in that callback some more state is changed, those callbacks should be wrapped in action as well!
This above mean, I should wrap state changed with runInAction, like this following:
class App {
#observable logined = false
#action async login(payload){
runInAction(() => {
setTimeout(() => {
this.logined = false
}, 1000)
})
}
}
Above works, but the weird is if i remove the runInAction block, code still works, this behaviour is inconsistent with the document said.
please check the fiddle.
This behavior is correct; unobserved data can be modified at will, as it cannot lead to further side effects, see: https://github.com/mobxjs/mobx/blob/master/CHANGELOG.md#310
Also not that it is always allowed to change state outside of actions as long as strict mode is not enabled (mobx.useStrict(true))
A PR to reflect this new behavior better in the docs would be appreciated! https://github.com/mobxjs/mobx/blob/gh-pages/docs/refguide/action.md
An "action" allows you to make multiple state changes in one "batch" -- if you are only making one change, you don't need it.

Resources