Reputation: 359
I've got a button that calls an async function, that is returned by a call to a custom React hook, alongside with a reactive prop that I need to keep track of.
// useEmail.js
import { useState } from "react";
export default function useEmail(message) {
const [returnedMessage, setReturnedMessage] = useState("old");
const send = async () => {
// fake fetch
const whatever = await fetch(
"https://jsonplaceholder.typicode.com/todos/1"
);
setReturnedMessage("new");
};
return {
returnedMessage,
send
};
}
And this is the app
// app.js
import React from "react";
import useEmail from "./useEmail";
export default function App() {
const { returnedMessage, send } = useEmail();
const run = async () => {
console.log("returnMessage PRE", returnedMessage);
await send();
console.log("returnMessage POST", returnedMessage);
};
return (
<div className="App">
<h2>Click and wait for 1 second</h2>
<button onClick={run}>Click me</button>
<h2>Returned message:</h2>
<p>{returnedMessage}</p>
<button onClick={() => window.location.reload()}>
Reload to test again
</button>
<p>
It prints "new", but logs "old"
<br />
even if I await send()...?
</p>
</div>
);
}
useEmail
returns both a returnMessage
string, that is initialized as "old"
, and an async function send
that fetches something, then flips the returnMessage
and sets it to "new"
.
How is it possible that in the <p>{returnedMessage}</p>
the value correctly turns from "old"
to "new"
, while the Console logs always "old"
, even if I await
when calling send()
?
It seems like send()
is not really treated as an asynchronous function – I've tried in different ways but I always have a correctly updated rendering but a wrong value when I need it in the function for further processing.
Thank you for your help
Upvotes: 5
Views: 16816
Reputation: 1651
You have 2 async functions in your custom hook.
setState
So even if you await for the fetch, your setState is still asynchronous:
console.log("returnMessage PRE", returnedMessage); //old
send()
returns undefined
(because no return is defined)console.log("returnMessage POST", returnedMessage); //old
returnedMessage
is updatedIf you want to have actions depending on when returnedMessage
is changed, you'll have to use useEffect
in your component
useEffect(() => {
if (returnedMessage === "old") return; // Do nothing here
// returnedMessage !== "old" so assume it's "new"
// Do something...
}, [returnedMessage]);
Upvotes: 1
Reputation: 8098
One thing that I noted, from your custom React Hook, you are returning an async
function.
which is this:
async () => {
// fake fetch
const whatever = await fetch(
"https://jsonplaceholder.typicode.com/todos/1"
);
setReturnedMessage("new");
};
And within your App Component, you are accessing the custom hook where send
is pointing to this async
function. Right?
Now when you are calling your async function you are trying to do:
await send();
Why await
here again, since we already have an await
inside of our function.
When you do this you are basically waiting for a promise()
here, since every async
function returns a promise even when nothing is returned.
I feel the implementation of custom hook should change or calling the hook has to be different.
On top of this setState()
is itself an asynchronous action. That is not in our control to tell when the state will update :)
Upvotes: 0
Reputation: 411
You can do the job using useRef
.
It seems you can't access the updated value without running the hook again.
With useRef
you'll get a reference and you can access the data at any time, without running the hook again.
// useEmail.js
export default function useEmail(message) {
const messageRef = React.useRef("old");
const send = async () => {
// fake fetch
const whatever = await fetch(
"https://jsonplaceholder.typicode.com/todos/1"
);
messageRef.current = "new";
};
return {
messageRef,
send
};
}
// app.js
export default function App() {
const { messageRef, send } = useEmail();
const run = async () => {
console.log("returnMessage PRE", messageRef.current);
await send();
console.log("returnMessage POST", messageRef.current);
};
return (
<div className="App">
<h2>Click and wait for 1 second</h2>
<button onClick={run}>Click me</button>
<h2>Returned message:</h2>
<p>{returnedMessage}</p>
<button onClick={() => window.location.reload()}>
Reload to test again
</button>
<p>
It prints "new", but logs "old"
<br />
even if I await send()...?
</p>
</div>
);
}
Upvotes: 2
Reputation: 980
It is a normal behaviour setState will produce only a single re-render at the end of the event even if you used await, try to add a console.log inside your component you will see returnedMessage moved to 'new'
// app.js
import React from "react";
import useEmail from "./useEmail";
export default function App() {
const { returnedMessage, send } = useEmail();
console.log("returnMessage POST", returnedMessage); // in last render it will be new so it will change the view
const run = async () => {
console.log("returnMessage PRE", returnedMessage);
await send();
};
return (
<div className="App">
<h2>Click and wait for 1 second</h2>
<button onClick={run}>Click me</button>
<h2>Returned message:</h2>
<p>{returnedMessage}</p>
<button onClick={() => window.location.reload()}>
Reload to test again
</button>
<p>
It prints "new", but logs "old"
<br />
even if I await send()...?
</p>
</div>
);
}
Upvotes: 0