React Native Async Call in Render Function - reactjs

This question is similar to some that have already been asked, such as (Can you make render() method in React Native async?), however none of these solutions I have found so far work for me. I am attempting to display a value from storage, however it needs to be obtained asynchronously, so when I am trying to render it returns an unresolved promise.
These functions store and get the data asynchronously:
const storeData = async () => {
try {
const value= "hello"
await AsyncStorage.setItem('#storage_Key', value)
} catch (e) {
}
}
const getData = async (key:string) => {
return (<Text>{await AsyncStorage.getItem('#storage_Key')}</Text>)
}
...and then in my return I am trying to do this:
<Button
onPress={() => storeData()}
title="Store"
color="#841584"/>
{getData()}
Which I want to display a button that saves data (working I think), and then display that data in a text field. However I keep receiving unresolved promise errors. How can I render something alternate until the value is loaded, and display the data without an unresolved promise?

You must use promise instead async/await function, and it is better to use functional components instead of class component.
First, you must use useEffect function for setState after AsyncStorage setItem:
const [asyncStorageItemSet, setAsyncStorageItemSet] = React.useState(false);
const [storageKey, setStorageKey] = React.useState('')
React.useEffect(() => {
AsyncStorage.getItem('#storage_Key').then(res => {
setStorageKey(res);
})
}, [asyncStorageItemSet])
and your return likes this:
<Button
onPress={storeData}
title="Store"
color="#841584"/>
<Text>{storageKey}</Text>
and storeData function looks like this:
const storeData = () => {
const value= "hello"
AsyncStorage.setItem('#storage_Key', value).then(res => {
setAsyncStorageItemSet(true)
})}

With functional components, broadly you want to
use the useState hook to create a variable/setter to hold the value you eventually get back from the promise.
use the useEffect hook to run the asynchronous process.
This means you will get at least 2 renders:
From before you have a value and
When you got the result
The first render the value will not be there.
If you haven't used the 2 hooks before, start by learning about them.

No, since the async functions are executed differently, the first instance of an asynchronous function does not return anything, in this way the render will return null which is worth a failure of the rendering of the component, that is maybe why we cannot directly call the Async function inside the render

Related

React useEffect with Stripe - get client secret asynchronously

I've been plopped into the middle of a react project with no prior knowledge so I'm sure this a duplicate.
I want to use the StripeElements, and the react library is easy enough. I'm just not sure how to get the client secret from the server.
This is what I'm trying:
const stripePromise = loadStripe('stripe_key');
const StripeForm = () => {
const [stripeData, setStripeData] = useState({});
const getClientSecret = async () => {
const { data } = await api.get(`payment/stripe/get-client-secret/`);
return data
}
useEffect(() => {
getClientSecret().then(data => setStripeData(data))
}, [])
if(stripeData.clientSecret) {
return (
<QuoteContainer title={"Pay With Credit Card"}>
<SectionCard>
{stripeData.clientSecret}
</SectionCard>
</QuoteContainer>
);
}
return (<b>Loading</b>)
}
The route payment/stripe/get-client-secret/ returns an array with a key 'clientSecret'. This function is working correctly.
I just can't get the <b>Loading</b> text to be replaced with the QuoteContainer component once the promise is resolved.
If you want to rendering Loading component. You have to set falsy value for stripedData state. Because {} value is truthy. So I think the code is not rendering Loading component. The loading component is not rendered because there is no place to set it as a false value anywhere.
what AJAX library are you using to make the API call? Usually you need to call a function on the response object in order to access the data. For instance, if you are using fetch(), you need to call json() then access the response data.

How to get current state value inside an async function

I have created some state at the top level component App() and created a getter method for this state so I could pass it on to any function to be able to get its current state.
App.js
const [searchState, changeScreen] = useState("");
const getSearchState = () => {
console.log("searchState is", searchState);
return searchState;
}
scripts/search.js
export const performSearch = async (searchText, changeScreen, getSearchState) => {
if(searchText) {
console.log("1", getSearchState())
let res = await doSearchQuery(searchText);
console.log("2", getSearchState())
if(res.status) {
// *** getSearchState() should have a value of "loading" here
if(getSearchState() !== "expanded") {
changeScreen("results");
}
}
else {
//
}
}
}
components/SearchComponent.js
import { performSearch } from '../scripts/search';
function SearchHistoryComponent({changeScreen, getSearchState}) {
...
// This method is fired from an onPress()
const performHistorySearch = async (text) => {
changeScreen('loading');
await performSearch(text, changeScreen, getSearchState);
}
...
}
I then pass getSearchState() as a parameter to a standalone asynchronous function in a different script to be able to look up the searchState value but it doesn't seem to be working as intended.
The value I'm getting seems to be the previous value and not the current value at the time getSearchState() is called - as can be seen from the console outputs I have setup:
searchState is expanded
searchState is loading
1 expanded
2 expanded
searchState is results
Am I doing something wrong?
This is exactly how React callback functions work.
That is, every line in your performSearch function is engaged to one searchState value, i.e, "expanded".
If you want to get "loading" from getSearchState(), you need to call getSearchState() again in useEffect by passing searchState or getSearchState in the dependency array.
To be more clear, setting a state value after awaiting a promise works fine, but if you want to pull the newly set state value inside the same function body, it won't work.
That said, you need to listen to the newly set state value in useEffect or just make your code declarative so it behaves according to state changes.
Just to help you understand this better, I've written a quick snack here to show the difference between pulling the state from a function body and a useEffect.
https://snack.expo.dev/h65-cPvIb
Thanks.

Set state on changed dependencies without additional render (having a loader without additional renders)

I have a huge component which renders long enough to be noticeable by user if it happens too often.
Its contents are loaded asynchronously (from a server) every time when a function "getData" changes, showing a loader during the wait.
I'm trying to write a code which will render the component only 2 times when the function changes - first time to show the loaded and the second time to display the data.
Using a standard useEffect causes it to be rendered 3 times, first of which doesn't change anything visible for the user.
type tData = /*some type*/
const Component = (props: {getData: () => Promise<tData[]>}) => {
const {getData} = props;
const [loader, setLoader] = useState(true);
const [data, setData] = useState<tData[]>([]);
useEffect(() => {
setLoader(true);
setData([]);
getData().then((newData) => {
setData(newData);
setLoader(false);
});
}, [getData, setLoader, setData]);
// ... the rest of the component (that doesn't use the function getData)
};
The three renders are:
getData has changed - the effect runs and changes the states but there is 0 changes visible to the user (this is the render I want to get rid of)
loader has changed to true and data has changed to [] - a useful render that actually changes the UI
loader has changed to false and data has changed to a new value - a useful render that actually changes the UI
How could I modify this code to not have a barren render when getData changes?
this is a very common use case.
what you can do here is a if() in the useEffect().
using a .then() complicates things. use async await to make it asynchronus.
useEffect(() => {
async function loadData() {
const res = getData()
return res
}
let data = await loadData()
//check if it's undefined
if (data && data!=='init') {
setStateData(data)
}
}, [])
this is how i'd implement something like this. however it seems like the data fetching for you is somewhat complicated then this, but essentially, never fetch data synchronously unless you absolutely have to, and check if it's undefined in useEffect before setting the state variable so that it's only set once.

React: Stop hook from being called every re-rendering?

Somewhat new to React and hooks in React. I have a component that calls a communications hook inside of which a call to an API is made with AXIOS and then the JSON response is fed back to the component. The issue I'm having is the component is calling the hook like six times in a row, four of which of course come back with undefined data and then another two times which returns the expected JSON (the same both of those two times).
I did a quick console.log to double check if it was indeed the component calling the hook mulitple times or it was happening inside the hook, and it is the component.
How do I go about only have the hook called only once on demand and not multiple times like it is? Here's the part in question (not including the rest of the code in the widget because it doesn't pertain):
export default function TestWidget() {
//Fetch data from communicator
console.log("called");
const getJSONData = useCommunicatorAPI('https://jsonplaceholder.typicode.com/todos/1');
//Breakdown passed data
const {lastName, alertList, warningList} = getJSONData;
return (
<h1 id="welcomeTitle">Welcome {lastName}!</h1>
);
}
export const useCommunicatorAPI = (requestAPI, requestData) => {
const [{ data, loading, error }, refetch] = useAxios('https://jsonplaceholder.typicode.com/todos/1', []);
console.log("data in Communicator:", data);
return {data};
}
I would use the useEffect hook to do this on mount and whenever any dependencies of the request change (like if the url changed).
Here is what you will want to look at for useEffect
Here is what it might look like:
const [jsonData, setJsonData] = React.useState({})
const url = ...whatver the url is
React.useEffect(() => {
const doFetch = async () => {
const jsonData = await useAxios(url, []);;
setJsonData(jsonData)
}
doFetch();
}, [url])
...use jsonData from the useState
With the above example, the fetch will happen on mount and if the url changes.
Why not just use the hook directly?
export default function TestWidget() {
const [{ data, loading, error }, refetch] =
useAxios('https://jsonplaceholder.typicode.com/todos/1', []);
return (<h1 id="welcomeTitle">Welcome {lastName}!</h1>);
}
the empty array [] makes the hook fire once when called
Try creating a function with async/await where you fetch the data.
Here can you learn about it:
https://javascript.info/async-await

Graphql subscriptions inside a useEffect hook doesn't access latest state

I'm building a basic Slack clone. So I have a "Room", which has multiple "Channels". A user subscribes to all messages in a Room, but we only add them to the current message list if the new message is part of the user's current Channel
const [currentChannel, setCurrentChannel] = useState(null);
const doSomething = (thing) => {
console.log(thing, currentChannel)
}
useEffect(() => {
// ... Here I have a call which will grab some data and set the currentChannel
Service.joinRoom(roomId).subscribe({
next: (x) => {
doSomething(x)
},
error: (err: any) => { console.log("error: ", err) }
})
}, [])
I'm only showing some of the code here to illustrate my issue. The subscription gets created before currentChannel gets updated, which is fine, because we want to listen to everything, but then conditionally render based on currentChannel.
The issue I'm having, is that even though currentChannel gets set correctly, because it was null when the next: function was defined in the useEffect hook, doSomething will always log that currentChannel is null. I know it's getting set correctly because I'm displaying it on my screen in the render. So why does doSomething get scoped in a way that currentChannel is null? How can I get it to call a new function each time that accesses the freshest state of currentChannel each time the next function is called? I tried it with both useState, as well as storing/retrieving it from redux, nothing is working.
Actually it is related to all async actions involving javascript closures: your subscribe refers to initial doSomething(it's recreated on each render) that refers to initial currentChannel value. Article with good examples for reference: https://dmitripavlutin.com/react-hooks-stale-closures/
What can we do? I see at least 2 moves here: quick-n-dirty and fundamental.
We can utilize that useState returns exact the same(referentially same) setter function each time and it allows us to use functional version:
const doSomething = (thing) => {
setCurrentChannel(currentChannelFromFunctionalSetter => {
console.log(thing, currentChannelFromFunctionalSetter);
return currentChannelFromFunctionalSetter;
}
}
Fundamental approach is to utilize useRef and put most recent doSomething there:
const latestDoSomething = useRef(null);
...
const doSomething = (thing) => { // nothing changed here
console.log(thing, currentChannel)
}
latestDoSomething.current = doSomething; // happens on each render
useEffect(() => {
Service.joinRoom(roomId).subscribe({
next: (x) => {
// we are using latest version with closure on most recent data
latestDoSomething.current(x)
},

Resources