React custom hook: can't get an async function - javascript

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

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

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

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

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 :)

Related

React: Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component

Hey i'm slightly new to react and wondering what i'm doing wrong...
What i'm trying to do:
Navbar with a connection button
onClick call CreateConnection
add Result to localstorage
add data to Navbar without refresh
Right now the useEffect doesn't recognize the new addition to localstorage so won't update until page refresh.
Navbar.js:
export const Navbar = () => {
const [connectionAddress, setConnectionAddress] = useState("");
useEffect(() => {
if (localStorage.getItem("address")) {
setConnectionAddress(localStorage.getItem("address"))
}
}, []);
return (
<ProfileNav
address={connectionAddress}
/>
<input type="submit" value="Connect" onClick={CreateConnection} />
)
}
CreateConnection.js
const CreateConnection = async () => {
try {
#Connection code - when this is finished:
window.localStorage.setItem('address', address[0]);
} catch (err) {
console.error(err);
}
}
ProfileNav.js
export const ProfileNav = ({ address }) => {
return (
<div>
<li>{address}</li>
</div>
)
}
export default ProfileNav;
If i simply add window.location.reload(true) to CreateConnection.js it works but i want it to work without a refresh (possibly with useState
But if i try to put the useState in CreateConnection.js like so:
try {
const [connectionAddress, setConnectionAddress] = useState("");
#Connection code - when this is finished:
window.localStorage.setItem('address', address[0]);
setConnectionAddress(address[0])
} catch (err) {
console.error(err);
}
}
i get the error when i click on the button: Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
As you've discovered, react hooks are only supposed to be called from React components or from other hooks. There's a few more rules that you should look into as well https://reactjs.org/docs/hooks-rules.html.
One option moving forward is to return the address from CreateConnection, and pass that into setConnectionAddress in the onClick handler (or a callback) in your component. Another option (less recommended) would be to create a parameter for setState in CreateConnection and pass setConnectionAddress into CreateConnection.
CreateConnection.js
We'll add the return, and also name this a bit more appropriately
// would recommend naming this with conventional camelCase
const createConnection = async () => {
try {
// Connection code - when this is finished:
window.localStorage.setItem('address', address[0]);
return address[0];
} catch (err) {
console.error(err);
}
}
Navbar.js
Here we a handler updateConnectionAddress to update the connection when the button is clicked. We could also put that code in the jsx within onClick's curly braces, but it's usually better to keep the jsx leaner.
export const Navbar = () => {
const [connectionAddress, setConnectionAddress] = useState("");
useEffect(() => {
if (localStorage.getItem("address")) {
setConnectionAddress(localStorage.getItem("address"))
}
}, []);
const updateConnectionAddress = async () = {
const newAddress = await createConnection();
setConnectionAddress(newAddress);
}
return (
<>
<ProfileNav address={connectionAddress} />
<input type="submit" value="Connect" onClick={updateConnectionAddress} />
</>
)
}
In the spirit of the single responsibility principle, I would likely consider moving the local storage code elsewhere from createConnection as well, but I think that's out of scope for this topic

Evaluation failed: ReferenceError: _MainBody is not defined - Puppeteer

I am super, super new to this subject (Today it is my first day): end 2 end unit tests and I discovered puppeteer.
I have my page where I launch a function to fetch some info from an API and than I display the info on the page.
What I want to do is the following.
I want to make this end 2 end test to check if the page has a header and a footer + if the function gets called and gives a response if called.
Bellow I will attach my code.
The question is: Why does it says that _MainBody is not defined since that is the name of the function and the file name where everything happens: fetch the data and display it.
I will attach it below so you can understand what I did and where is the problem.
Thank you in advance to everyone that is willing to help.
e2e.test.tsx
import getRandomBeer from "./MainBody";
import puppeteer from "puppeteer";
describe("myTest", () => {
let browser: puppeteer.Browser;
let page: puppeteer.Page;
beforeAll(async () => {
browser = await puppeteer.launch();
page = await browser.newPage();
});
it('The function gets called', async () => {
await page.goto('LINK');
console.log(await page.evaluate(() => typeof getRandomBeer === 'function'));
})
afterAll(() => browser.close());
});
file where everything happens and where the function gets called to fetch the data
import { render } from '#testing-library/react';
import React, { useState, useEffect } from 'react';
import './App.css';
import axios, { AxiosResponse } from 'axios';
import Beer from './BeerClass';
//Function that gets called in order to fetch the beers one by one
async function getRandomBeer() {
const req = await fetch('https://api.punkapi.com/v2/beers/random');
const data = await req.json();
console.log(data[0]);
return data[0] as Beer;
}
const nBeers = 30;
function MainBody() {
const [beerData, setBeerData] = useState<Beer[]>([]);
console.log(beerData);
//-----------------------------------------------------------------------------------------------------------------
//NOTE: Some of the beers come with NULL for the image link so some of the beers don't have a photo unfortunatelly.
//-----------------------------------------------------------------------------------------------------------------
//Saving all the beers inside an array that initally gets filled with zeros and than I map the beers inside it and than I se the beerData so I can display it below
//It waits until it does not fetch all the beers
useEffect(() => {
Promise.all(new Array(nBeers).fill(0).map(getRandomBeer).reverse()).then(setBeerData);
}, [])
//Display the beer data, beer after beer
return (
<div id="beers">
{beerData && beerData.map((beerData) => {
return (
<div className="container">
<div className="image"> <img src={beerData.image_url} width={30} height={100}></img>
<div className='text'>
<h4>{beerData.name} </h4>
<p>{beerData.tagline}</p>
</div>
</div>
</div>
);
}
)}
</div>
);
};
export default { MainBody , getRandomBeer};
You need to use this after defining page
now you can use your function in the callback you use in evaluate.
evaluate returns the element you define
await page.exposeFunction("getRandomBeer", getRandomBeer);

React Custom Hook make return Function executable on an Event

I have created a custom hook for handling the uploads to AWS s3 bucket but I am facing a small problem. I did not want my hook to execute the logic directly so I created an executable function that I am then returning. The only problem is that the state is not being set from inside the handleUpload function
Here is what I am trying to do:
import React, { useState } from "react";
const useS3Aws = () => {
const [isLoading, setIsLoading] = useState(false);
const [uploadedUrl, setUploadedUrl] = useState("");
const handleUpload = async (file: any, s3FolderPath: string) => {
setIsLoading(true); // the problem is here (state not being updated)
// handle the uploading with AWS s3
// code ......
// setting the state with returned live url from s3
setUploadedUrl("the live url"); // the problem is here (state not being updated)
console.log(uploadedUrl); // it prints an empty string
setIsLoading(true); // the problem is here (state not being updated)
};
return [
isLoading,
uploadedUrl,
(file: any, s3FolderPath: string) => handleUpload(file, s3FolderPath),
] as const;
};
const UploadSong = () => {
const [isLoading, uploadedUrl, handleUpload] = useS3Aws();
const handleSong = async () => {
const file = {
// object data
};
handleUpload(file, "music");
console.log(uploadedUrl); // it is empty
};
return (
<div>
<p>Live URL: {uploadedUrl ? uploadedUrl : "No Link"}</p>
<button onClick={() => handleSong()}>
Click me
</button>
</div>
);
}
export default UploadSong;
Playground Link
Your hook looks fine, but you aren't using it's returned values right.
<button onClick={() => handleSong}>
That doesn't actually call handleSong on click. This calls a function that merely returns the handleSong function on click.
You want:
<button onClick={() => handleSong()}>
Or if you don't want to pass arguments, you can just:
<button onClick={handleSong}>
One point where you may be confused here:
console.log(uploadedUrl); // it is empty
When you set state, it's asynchronous. State is being set properly, but any local variables will reflect the previous state, because the re-render has not yet happened. Usually this doesn't matter at all, because as you can see here the UI updates immediately when you click the working button.

React hooks not set state at first time

i have a problem with hooks.
i'm using react-hooks, i have a button which at onClick getting data from api and setState with it.
Problem is:
when i click to button first time i get response from api but can't set it to state. When click to button second time i can setState. Why it happened ?
here is my component look like:
function App() {
const [a, setA] = useState(null);
const fetchData = () => {
let data = {
id: 1
}
axios.post(baseUrl, data)
.then(res => {
if (res.data) {
setA(res.data)
}
console.log(a)
})
}
return (
<div className="App">
<div>
<button onClick={fetchData}></button>
</div>
</div>
);
}
export default App;
i tried to using fetchData function like that:
function fetchData() {
let data = {
id: 1
}
axios.post(baseUrl, data)
.then(res => {
if (res.data) {
setA(res.data)
}
console.log(a)
})
}
but it's not helped too
a is a const. It's impossible to change it, so there's no way your console.log statement at the end of fetchData could ever log out something different than the value that a had when fetchData was created.
So that's not what setA does. The point of setA isn't to change the value of the const, but to cause the component to rerender. On that new render, a new set of variables will be created, and the new a will get the new value. Move your console.log out to the body of your component, and you'll see it rerender with the new value.
In short: Your code appears to be already working, you just have the wrong logging.
If your scope is to fetch data, use this:
const [data, setData] = useState("");
useEffect(async () => {
const result = await axios(
'here will be your api',
);
setData(result.data);
});
Now your response will be stored in data variable.
I would not use an effect for it, effects are useful if the props or state changes and can thereby substitute lifecycle methods like componentDidMount, componentDidUpdate, componentWillUnmount, etc.. But in your case these props haven't changed yet (you want to change your state though). Btw, be aware that #Asking's approach will fetch data on EVERY rerender (props or state change). If you want to use useEffect, be sure to add the second parameter to tell React when to update.
Normally, your code should work, I haven't tested but looks legit. Have you used developer tools for react to check if the state/hook has changed? Because if you say it did not work because of the console.log printing: Have in mind that setA() is an async function. The state was most likely not yet changed when you try to print it. If you haven't react developer tools (which I highly recommend) you can check the real state by adding this code in the function:
useEffect(() => console.log(a), [a]);
I have a few real improvements to your code though:
function App() {
const [a, setA] = useState(null);
const fetchData = useCallback(async () => {
let data = {
id: 1
}
const res = await axios.post(baseUrl, data);
setA(res.data);
}, []);
return (
<div className="App">
<div>
<button onClick={fetchData}></button>
</div>
</div>
);
}
export default App;
By adding useCallback you ensure that the function is memorized and not declared again and again on every rerender.
#Nicholas Tower has already answered your question. But if you are looking for code
function App() {
const [a, setA] = useState(null);
const fetchData = () => {
let data = {
id: 1
}
axios.post(baseUrl, data)
.then(res => {
if (res.data) {
setA(res.data)
}
})
}
console.log(a)
return (
<div className="App">
<div>
<button onClick={fetchData}></button>
</div>
</div>
);
}
export default App;
just place log before return (. This should do

What happens if we call an async function inside render method?

We do not use redux or saga and use services instead, for making API calls and storing data. Consider that there is a button that will make an async call when clicked.
Is it safe to call the async function inside the render method?
Would it make the UI unresponsive until the promise is resolved?
render() {
return (
<Button onPress={() => {await this.requestOtp()}} text="Get OTP" />
);
}
Try to use hooks:
import React, { useState } from 'react';
function MyComponent(){
let [output, updateOutput] = useState(null);
async function asyncFunction(){
// call here
let response = await this.requestOtp();
updateOutput(response.data);
}
return <Button onPress={asyncFunction} text={output} />
}
Render can not be async you can pass the async to componentDidMount or use hooks

Categories

Resources