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

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

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

How to call a function in useEffect() outside of it

I'm trying to call a function in useEffect() by doing this:
const myComponent = () => {
useEffect(() => {
const aFunc = () => {
console.log("function called")
}
}, [])
return (
<button onClick={() => aFunc()} />
)
}
As you can see, everytime I click on the <button> I would like to call the aFunc() function.
How can I call the aFunc outside of useEffect() but in the same component? It seems that React can't recognize the function when I bind it in the onClick
You can't. The function exists in a different scope.
The point of useEffect is to isolate code so it doesn't rerun every time a component renders. The most common use for this is to make an Ajax request for data when the component is added to the document without triggering an infinite loop as every Ajax response causes the component to rerender and trigger the Ajax request to be made again.
Just declare the function directly inside the component or, since the function in this particular example doesn't depend on any data from the component, entirely outside the component.
If your example is massively over simplified and the process of creating the function is expensive and dependent on data in the component: Look at useCallback.
You dont need to use useEffect for this,
const myComponent = () => {
const aFunc = () => {
console.log("function called")
}
return (
<button onClick={() => aFunc()} />
)
}
Adding on to Dostonbek Oripjonov's answer
Just declare the function outside the useEffect hook and then call it from inside the hook.
This would allow you to use that function in other places too!
I think you want this
const myComponent = () => {
const aFunc = () => {
console.log("function called")
}
useEffect(() => {
aFunc()
}, [])
return (
<button onClick={() => aFunc()} />
)
}

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

Infinity Loop with promises and state variables in ReactJs

My react component is rendering multiple times in infinity loop. What am I missing?
Here is my code.
const DocViewer = ({ title, closeModal }) => {
const [docsSimilares, setDocsSimilares] = useState([]);
const baseUrl = '/docs'
async function similares() {
return await axios.get(`${baseUrl}/${title}`).then(data => setDocsSimilares(data.data.body.hits.hits[0]._source.documentos_similares))
}
similares().then(console.log(docsSimilares))
return (
<div class="row">
<div class="column pdf">
<h1>{title}</h1>
<PDFViewer url={sFileNameDemo} />
</div>
<div class="column semelhantes">
<button onClick={closeModal} >Fechar</button>
<p>{docsSimilares.map(doc => (
<div>
<p>{doc}</p>
<img alt={doc} src={doc} width="100%" />
</div>
))}</p>
</div>
</div>
);
}
export default DocViewer
When you call your similares() function you are setting state when calling setDocsSimilares.
By setting state you trigger re-render, when you trigger re-render it calls your similares() function again, that changes the state... You got the idea :)
By simply calling similares() inside of your functional component - it executes on each re-render.
You cannot just simply call similares() the way you do.
I think what you trying to achieve is to get data on first component mount, so you should use useEffect with empty array as second argument like so:
useEffect(() => similares().then(console.log(docsSimilares)),[])
If you are using hooks you need to understand useEffect
useEffect(() => similares().then(console.log(docsSimilares)),[])
The square brackets are where you add dependencies, if no dependencies it will only run on initializing "mounting" it is the same as componentDidMount() so that it will only fire the async request on load.
This code solved my problem:
async function similares() {
const simDocs = await axios.get(`${baseUrl}/${title}`).then(data => (data.data.body.hits.hits[0]._source.documentos_similares))
setDocsSimilares(simDocs)
}
useEffect(() => {
similares().then(data => console.log(data))
},[]);

use componentWillMount without a class

with react-native, I want to use componentWillMount without using a class
await Font.loadAsync({
gotham_medium: require("../../assets/GothamMedium_1.ttf")
});
}
const Button = (props: TouchableOpacityProps & ButtonProps) => (
<TouchableOpacity {...props} style={styles.button}>
<Text style={styles.title}>{props.title}</Text>
</TouchableOpacity>
);
export default Button;
But I have a problem on the device :
error on the device
It says the problem is on this line (and it is):
async componentWillMount = () => {
When you use an async function, the async keyword goes right before () => (a vanilla js syntax error). Like this:
componentWillMount = async () => {
But, that's not the main problem. When not using a class, you need the useEffect hook.
So, try something like this (the whole component, and deleting componentWillMount):
const Button = (props: TouchableOpacityProps & ButtonProps) => {
useEffect(async () => {
await Font.loadAsync({
gotham_medium: require("../../assets/GothamMedium_1.ttf")
});
}, []);
return (
<TouchableOpacity {...props} style={styles.button}>
<Text style={styles.title}>{props.title}</Text>
</TouchableOpacity>
);
};
And at the top of the file:
import { useEffect } from 'react';
You can use Hooks for this,
from the docs,
If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
And
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.
useEffect(async () => {
await Font.loadAsync({
gotham_medium: require("../../assets/GothamMedium_1.ttf")
});
},[]);

Categories

Resources