React - Unable to setState inside a Promise then - javascript

I'm trying to set the state inside a then() of a Promise, but the state value is not getting saved and is not accessible outside of the then().
Below is the UPDATED code:
handleSelect = location => {
this.setState({ location });
geocodeByAddress(
location
)
.then(results => getLatLng(results[0]))
.then(latLng => {
this.setState({
location_latitude: latLng.lat,
location_longitude: latLng.lng,
})
console.log(this.state.location_latitude); // returns the correct value
})
console.log(this.state.location_latitude); // returns null (the originally declared value)
}
If I console.log(this.state.location_latitude) I get a null value. But if I console.log inside the then() block, I get the correct value. How do I set the state in this case?
UPDATED explanation:
To give a better context to the whole logic here: I'm using a Google Places Autocomplete component which allows the user to select a location from the dropdown. The above method handleSelect is called when the user selects a location from the dropdown. I then extract the latitude and longitude of the place selected by the user using the geocodeByAddress method, which is a promise. I need to then retrieve the latitude and longitude and set the state accordingly, which is later used to send to the database. I can't submit the value to the database yet because there are other form inputs that the user needs to fill (which are also stored in the state) and once he clicks on Submit, I'll be submitting all the state elements in a single call to the backend. So, there is nothing I want to update with componentDidMount() or nothing is getting updated yet to use componentDidUpdate(). In any case, I can only store the state value inside the then of geocodeByAddress (please correct me if I'm wrong here). So, I HAVE to be able to modify the state value inside the then block.

The thing is when you set-state you can't see the state result immediately after that. That's why the console.log returns null. Make sure your console.log is in the render block. This is because set-state actions are asynchronous and are batched for performance gains. This is explained in documentation of setState.
Take a look at this => Why is setState in reactjs Async instead of Sync?

You need to bind the function context to current class/ component. Try something like below. Create a function like "callSetState" bind it with this and while calling in then just pass the values as parameters to it.
import React, { Component } from 'react';
class Test extends Component{
constructor() {
super()
this.callSetState = this.callSetState.bind(this);
}
callSetState(latitude, longitude) {
this.setState({
location_latitude: latitude,
location_longitude: longitude,
})
}
/* Call this func in your class and call callSetState which is already bound to your component in constructor */
// geocodeByAddress(location)
// .then(results => getLatLng(results[0]))
// .then(latLng => this.callSetState(latLng.lat, latLng.lng));
}

This is clearly an issue with your data call not react.
Your react code looks fine but I suggest spitting the code.
1) service.js file - this does your query and returns data or returns error.
2) location component, componentDidMount() or useEffect(() => {}, []) gets the data and updates state/useState()
—
To give you a code snippet I need to you to provide a console log of latLang & results.
Many thanks 👌🏻

Related

Having promise response inside Vue template interpolation

I'm using Vue 2 and the composition API. My current component receives a prop from a parent and I render different data based on it, on the screen - the prop is called "result" and is of type Object, containing multiple info. I receive one "result" at a time, but multiple will be rendered - you can think of my "result" as a google search result - which means that the page will have multiple results.
The issue I have is that for one of the info inside "result", I need to call an asynchronous method and display its result, which is what I cannot accomplish.
Currently this is what I have:
<div>
{{ getBoardName(props.result.boardReference) }}
</div>
Method inside the script:
async function getBoardName(boardReference) {
var result = await entriesApi.getBoardName({
boardReference: boardReference,
});
return result;
}
It displays "[object Promise]", although if I console.log(result) right before returning it, it's just what I need, so it seems to me as if the interpolation doesn't actually wait for the promise result.
I've also tried using "then" inside the interpolation:
{{
getBoardName(props.result.boardReference).then((value) => {
return value;
})
}}
I was thinking about using a computed property but I am not sure how that would work, as the response I need from the method has to be connected to each result individually.
Please ask for further clarifications if needed.
As you thought, the interpolation does not actually wait for the result so this is why you have a Promise object.
Since you are using the composition API, what you can actually do is to use a reactive state (using the ref() function if you are waiting for a primitive type, which seems to be the case here, or the reactive() function for objects) and update the state within the onMounted() lifecycle hook.
setup(props, context) {
const boardGameName = ref("");
onMounted(async () => {
boardGameName.value = await getBoardName(props.result.boardReference);
});
async function getBoardName(boardReference) {
var result = await entriesApi.getBoardName({
boardReference: boardReference,
});
return result;
}
return {
boardGameName,
};
}
Since you are dealing with async data, you could add a loading state so you can show a spinner or something else until the data is available.
If your board reference changes over time, you could use a watcher to update your board game name.
Good luck with your project!

Getting the true value of an object from a fetched source in Vue.js

I'm getting a list of countries by fetching data from an external API in VueX. I then use a mutation to store these list of countries to a state.
So in effect, I'm storing the countries in a variable that I refer to as this.$state.getters.countryList from a component. Now I need to compare if a variable exists in that list.
So I use this method:
this.$store.getters.countryList.indexOf('usa').
I know for a fact that 'usa' exists in that list, but I always get -1 as a result.
I then tried to debug the value of this.$state.getters.countryList by using console.log(this.$state.getters.countryList) and I get [__ob__: Observer] as the result in the console.
This is what I'm trying to do:
mounted() {
if (this.$store.getters.countryList.indexOf('usa')) {
//Do something
}
}
How can I get the actual value of this.$state.getters.countryList and use that in conjunction with indexOf() to perform a check to do something else?
Since fetching data from external API is an async operation, the data you are trying to access might not be available at the time when you need it as mentioned by #Phil.
For that reason you could set a watcher to run your set of instructions once the fetch operation is completed and countryList is loaded:
In store.js, let's say you got your countryList that is set to null in your store initially:
export const state = () => ({
countryList: null
})
Then in your mounted() hook you would set a watcher:
mounted() {
this.$store.watch(
state => state.countryList,
(value) => {
if (value) {
//instruction once countryList is loaded
}
}
)
}

React - Populating First Select Option with Redux State

I have component League.js where there are 4 boxes containing League team details.
The team details are coming from this API => https://www.api-football.com/demo/api/v2/teams/league/${id}
When I click on each of the league boxes I am populating the Dropdown in my component Details.js
I won't get the team_id of the first team in the dropdown anytime I click on each of the League boxes in League.js in order to have the right calculation in a component called Stat.js. To do this I make a call request to this endpoint => https://www.api-football.com/demo/api/v2/statistics/${league}/${team}
So I need to pass league_id and team_id as a parameter, I can get both values correctly but I am doing something wrong on how to pass the first team_id of the team for each league.
These are the steps, I put only the relevant code. I am creating firstTeamStats state that I am dispatching
In my actions => index.js
export const RECEIVE_FIRST_TEAM_STATS = "RECEIVE_FIRST_TEAM_STATS";
export const receivedFirstTeamStats = json => ({
type: RECEIVE_FIRST_TEAM_STATS,
json: json
});
.get(`https://www.api-football.com/demo/api/v2/teams/league/${id}`)
.then(res => {
let teams = res.data.api.teams;
dispatch(receivedFirstTeamStats(teams[0].team_id));
})
in my reducer => index.js
case RECEIVE_FIRST_TEAM_STATS:
return {
...state,
firstTeamStats: action.json,
isTeamsDetailLoading: false
};
in my League.js component
import {getTeamsStats} from "../actions";
const mapStateToProps = state => ({
firstTeamStats: state.firstTeamStats
});
const mapDispatchToProps = {
getStats: getTeamsStats,
};
const onClick = (evt, id, firstTeamStats) => {
evt.preventDefault();
getDetail(id); \\ get the team names in the Detail.js component
getStats(id, firstTeamStats); \\ get the state value for the first team in the dropdown in Detail.js component
};
<a href={`#${item.league_id}`} onClick={(e) => onClick(e, item.league_id, firstTeamStats)}
Right now firstTeamStats in the onclick method above returns correctly the first team state value but of the existing league and not in the one where I click which is what I want.
However I can get this correctly in the Details.js Component, I have put {firstTeamStats} there in the demo where I have reproduced my case => https://codesandbox.io/s/romantic-solomon-6e0sb (Use CORS Unblock Chrome extension to see )
So the question is, how can i pass {firstTeamStats} correctly in League.js in the method onclick to the request getStats(id, firstTeamStats) where {firstTeamStats} is the state of my first team_id of the league?
Whenever you are making an API call, it is an asynchronous event. You should always await, before using its response in the next line.
So inside getDetail(), you are making an API call. But you haven't declared this method as async. so you are not waiting for it to complete and in the next line you are calling getStats() method with an old data. Thats why your app will always be a step behind, than your DOM events.
One way would be to use async|await and return the API response
from the function, instead of using dispatch.
You can also invoke the getTeamsStats() inside the getTeamsDetailById() after the api response is fetched, instead of invoking it inside OnClick()
event.
const onClick = (evt, id, firstTeamStats) => {
evt.preventDefault();
getDetail(id); \\ this is an async api call. you should have used await here.
getStats(id, firstTeamStats); \\ It will execute before the new stats are fetched
};
In Stats.js, you should not be strictly validating these fields to show Statistics... these are only numbers.. can also be 0 at times. so please remove these checks.
if (
teamsStatsWinHome &&
teamsStatsWinAway &&
teamsStatsDrawHome &&
teamsStatsDrawAway &&
teamsStatsLoseHome &&
teamsStatsLoseAway
) {
I have a working instance of your application... check below.
https://codesandbox.io/s/charming-dewdney-zke2t
Let me know, if you need anything.

Where to act on changed child properties in React JS

I'm confused, and I have searched a lot for the answer to this (seemingly) basic question.
I'm learning React, and I have a rather common component hierarchy with one top component (lets call it App) which contains a number of subcomponents (a grid, a graph, a table etc).
They all show information regarding one product.
Now when I select a row in the grid, I want to inform the other subcomponents about the change. App therefore passes a callback method
onSelectedProduct={this.onSelectedProduct}
to the grid. This gets called OK. In this App method I set the state:
onSelectedProduct(product) {
this.setState({ product: product });
}
In its render(), App has declared another subcomponent:
<ProductGraph product={this.state.product} />
Since ProductGraph needs to fetch some data asynchronously "to-be-rendered" later, where should I catch this property change??
The old "componentWillReceiveProps" sounded like the proper place, but will be deprecated and should not be used, I understand.
I have also tried shouldComponentUpdate, getDerivedStateFromProps and even to catch it in render, but they all have downsides and eventually lead to horrible code.
Somewhere, somehow, I should be able to detect that props.product !== state.product and issue an async load call for the data...
When the async method I call returns with the data, it will set the state and render itself.
So where is the optimal place to catch changed properties?
I have read a lot about the React Lifecycle but I just can't seem to find this basic information. Am I stupid or maybe blind? Or have I got this completely wrong somehow?
You are looking for componentDidUpdate() the lifecycle method that triggers when a component receives new props or has an updated state.
https://reactjs.org/docs/react-component.html#componentdidupdate
In your ProductGraph component, you would do something like:
componentDidUpdate(prevProps){
if(this.props.product !== prevProps.product){
fetch(`myApi/${this.props.product}`)
.then(res => res.json())
.then(data => this.setState({ data: data })) <-- identify what you need from object
.catch((errors) => {
console.log(errors)
})
}
}

Attempt to render state value from ajax call in componentDidMount failing (React)

I've made an AJAX call in React with Axios, and I'm a bit confused about how the response is dealt with.
Here is my code:
componentDidMount() {
axios.get('https://jsonplaceholder.typicode.com/users')
.then( res => {
const users = res.data
this.setState({ users });
});
}
render() {
console.log(this.state.users)
return (
<div>
{this.state.users.map( user => <p>{user.name}</p>)}
</div>
)
}
When I console.log the results, two values are returned:
[] - the empty array assigned to the initial state
The expected array from the API.
This presumably can be explained by the state value being returned once on initialisation, and then again with componentDidMount.
My problem arises in how I access the this.state.users value. Obviously any time I access this, I want the values returned from the AJAX response, but if, for example I try to render the name property from the first object in the array...
{this.state.users[0].name}
...it attempts to access [], and thus returns nothing.
However, if I try to iterate through the arrays elements...
{users.map( user => <p>user.name</p> )}
...it returns the values as expected.
I don't know why it works with the second example but not the first. Surely if the first is attempting to pull data from the initial state value (an empty array), then when mapping through this.state.users it would also be attempting to map through an empty array and return nothing, or an error.
I'm obviously not fully understanding the React rendering process here and how componentDidMount works in the component lifecycle. Am I wrong in trying to assign the response value directly to state?
Would appreciate if anyone could help to clear this up.
When you use map() on this.state.users initially, it will do so over an empty array (which won't render anything). Once the response arrives, and this.setState({ users }) is called, render() will run again. This time, the array of users is no longer empty and the map() operation will be performed on each of the users in the array.
However, when you use {this.state.users[0].name} initially on the empty array, you're trying to access the name property of an object that doesn't yet exist — which will result in an error. To prevent the error, you could do a check before accessing the properties of the object:
{this.state.users.length > 0 && this.state.users[0].name}
Now it will behave the same way as in the first example. I.e. the first render() won't actually do anything, since this.state.users.length = 0. But as soon as this.setState() is called with new users, render() will run again, and this time this.state.users[0].name is available.
There is another way with constructor. just add your state with empty array users as-
constructor(props) {
super(props);
this.state = {
users: []
};
}
With this component first rendered with empty users array. When you trigger state update, component rerender with new array.

Categories

Resources