Davide Barranca
Davide Barranca

Reputation: 359

React custom hook: can't get an async function

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.

CodeSandbox here.

// 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()?

enter image description here

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

Answers (4)

dbuchet
dbuchet

Reputation: 1651

You have 2 async functions in your custom hook.

  • Your fetch (which one you await)
  • setState

So even if you await for the fetch, your setState is still asynchronous:

  • console.log("returnMessage PRE", returnedMessage); //old
  • Fetch
  • Await fetch to complete
  • Fetch complete
  • trigger setState
  • function send() returns undefined (because no return is defined)
  • console.log("returnMessage POST", returnedMessage); //old
  • State is updated (async setState is complete)
  • returnedMessage is updated
  • Component re-renders

If 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

Imran Rafiq Rather
Imran Rafiq Rather

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

jean-smaug
jean-smaug

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>
  );
}

enter image description here

Upvotes: 2

ARZMI Imad
ARZMI Imad

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

Related Questions