I am fetching data from within componentDidMount as
this.setState({ isLoading: true });
fetch(
`https://api.example.com/location/12345`
)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Something went wrong ...');
}
})
.then(data => this.setState({ data, isLoading: false }));
and this works absolutely fine. But if I want to replace https://api.example.com/location/12345 with https://api.example.com/location/${this.props.id} to allow the id to change I get back errors that the data does not exist.
This is clearly because the fetch inside componentDidMount is fetching the url before before the this.props.id is read.
How can I delay the fetch until this.props.id is available?
One way is, use componentDidUpdate lifecycle method to get the data whenever component receive new id, but make sure to compare the prev id value with new id value and do the call only when they are not same.
Like this:
componentDidUpdate(prevProps) {
if(this.props.id && (prevProps.id != this.props.id)) {
this._getData();
}
}
_getData(){
this.setState({ isLoading: true });
fetch(
`https://api.example.com/location/${this.props.id}`
)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Something went wrong ...');
}
})
.then(data => this.setState({ data, isLoading: false }));
}
I use this pattern quite often :
initFromProps(props: MyComponentProps) {
const { } = props;
}
componentWillMount() {
this.initFromProps(this.props);
}
componentWillReceiveProps(nextProps: MyComponentProps) {
this.initFromProps(nextProps);
}
This ensures your component does whatever is necessary when the props change, but also at startup. Then, in initFromProps, you can do something along the lines of this:
initFromProps(props: MyComponentProps) {
const { id } = props;
if (id !== this.props.id) {
this._getData(id);
}
}
Related
This is a simple question. How do I successfully update state object via react hooks?
I just started using hooks, and I like how it allows to use the simple and pure JavaScript function to create and manage state with the useState() function, and also, make changes that affect components using the useEffect() function, but I can't seem to make update to the state work!
After making a request to an API, it return the data needed, but when I try to update the state for an error in request and for a successful request, it does not update the state. I logged it to the browser console, but no change was made to the state, it returns undefined.
I know that I'm not doing something right in the code.
Here is my App component, Its a single component for fetching and updating:
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
export default function App() {
// Set date state
const [data,setData] = useState({
data: [],
loaded: false,
placeholder: 'Loading'
});
// Fetch and update date
useEffect(() => {
fetch('http://localhost:8000/api/lead/')
.then(response => {
if (response.status !== 200) {
SetData({placeholder: 'Something went wrong'});
}
response.json()
})
.then(result => {
console.log(data);
setData({data: result});
});
},[]);
return (
<h1>{console.log(data)}</h1>
);
}
ReactDOM.render(<App />, document.getElementById('app'));
There are a few things you can improve:
the react-hook useState does not behave like the class counterpart. It does not automatically merge the provided object with the state, you have to do that yourself.
I would recommend if you can work without an object as your state to do so as this can reduce the amount of re-renders by a significant amount and makes it easier to change the shape of the state afterwards as you can just add or remove variables and see all the usages immediately.
With a state object
export default function App() {
// Set date state
const [data,setData] = useState({
data: [],
loaded: false,
placeholder: 'Loading'
});
// Fetch and update date
useEffect(() => {
fetch('http://localhost:8000/api/lead/')
.then(response => {
if (response.status !== 200) {
throw new Error(response.statusText); // Goto catch block
}
return response.json(); // <<- Return the JSON Object
})
.then(result => {
console.log(data);
setData(oldState => ({ ...oldState, data: result})); // <<- Merge previous state with new data
})
.catch(error => { // Use .catch() to catch exceptions. Either in the request or any of your .then() blocks
console.error(error); // Log the error object in the console.
const errorMessage = 'Something went wrong';
setData(oldState=> ({ ...oldState, placeholder: errorMessage }));
});
},[]);
return (
<h1>{console.log(data)}</h1>
);
}
Without a state object
export default function App() {
const [data, setData] = useState([]);
const [loaded, setLoaded] = useState(false);
const [placeholder, setPlaceholder] = useState('Loading');
// Fetch and update date
useEffect(() => {
fetch('http://localhost:8000/api/lead/')
.then(response => {
if (response.status !== 200) {
throw new Error(response.statusText); // Goto catch block
}
return response.json(); // <<- Return the JSON Object
})
.then(result => {
console.log(data);
setData(data);
})
.catch(error => { // Use .catch() to catch exceptions. Either in the request or any of your .then() blocks
console.error(error); // Log the error object in the console.
const errorMessage = 'Something went wrong';
setPlaceholder(errorMessage);
});
},[]);
return (
<h1>{console.log(data)}</h1>
);
}
The correct way to update an Object with hooks it to use function syntax for setState callback:
setData(prevState => {...prevState, placeholder: 'Something went wrong'})
Following method will override your previous object state:
setData({placeholder: 'Something went wrong'}); // <== incorrect
Your final code should look like this:
.then(response => {
if (response.status !== 200) {
setData(prevObj => {...prevObj, placeholder: 'Something went wrong'});
}
return response.json()
})
.then(result => {
setData(prevObj => {...prevObj, data: result});
});
I have a component with a button which handles some stuff, i want to pass an interceptor to this component so i can call an API inside the interceptor and ask for permission, if permission is granted the code inside the component's button's onClick is executed and if not, well it is not
So right now i'm having trouble figuring out what to do, here is a sudo code showing what i want to do:
//Inside componentA which is using componentB
onClickInterceptor = () => {
axios.post(//something)
.then(response => {
// do a few thing and finally you know if you should return true or not
})
.catch(error => {
//you know you don't have permission
})
return //This is my problem, i don't know what to return, i don't want to return the axios post, i want something like a new promise ?
}
//Inside componentB
onButtonClick = (event) => {
if (this.props.onClickInterceptor) {
this.setState({ disableButton: true })
this.props.onClickInterceptor()
.then(permissionState) => {
if (permissionState) {
this.runTheMainCode()
}
else {
//dont do anything
}
this.setState({ disableButton: false })
}
}
else
this.runTheMainCode()
}
this.runTheMainCode() {
//...
}
Right now i don't know what to return inside onClickInterceptor, i know i don't want to to return the axios, but how can i return a promise like that which only returns true or false ?
By the way, i want the button to be disabled until the interceptor is done
You need to return the axios promise and handle it at the componentB
//Inside componentA which is using componentB
onClickInterceptor = () => {
return axios.post(//something)
.then(response => {
// you can return false or true here
return true
})
.catch(error => {
throw error
})
}
//Inside componentB
onButtonClick = (event) => {
this.props.onClickInterceptor.then(returnedBoolean=>{
// here will come what you returned
this.setState({ disableButton: returnedBoolean })
}).catch(err=>{
// the error hadnling ....
this.setState({ error: true })
})
}
For some reason, I am unable to update the state of my component with the data from my fetch request.
When I console.log, I can see that I am getting the data. I'm not sure what this could be
If this is a noob issue, please bear with me. I'm still learning.
Here is my code:
import React, { Component } from "react";
class Nav extends Component {
state = {
searchTerm: "",
posts: []
};
getPost = e => {
e.preventDefault();
const val = e.target.value;
this.setState({ searchTerm: val }, () => {
if (val !== "") {
fetch(
`http://www.reddit.com/search.json?q=${val}&sort=relevance&limit=25`
)
.then(res => res.json())
.then(data => console.log(data.data))
//.then(data => this.setState({ posts: data.data }))
//.then(console.log(this.state.posts))
.catch(err => console.log(err));
}
});
};
Actually everything is right and going well, just your logging is wrong.
.then(console.log(this.state.posts))
That logs the state now and passes the result of console.log() (undefined) to the .then chain as a callback which is obviously wrong. I guess you meant:
.then(() => console.log(this.state.posts))
But that still won't work as setState does not trigger a state update immeadiately, but somewhen. After that it calls the second argument as a callback, so you should log then:
.then(data => this.setState({ posts: data.data }, () => {
console.log(this.state.posts);
}))
Altogether:
const response = fetch(
`http://www.reddit.com/search.json?q=${val}&sort=relevance&limit=25`
).then(res => res.json());
// PS: I would not build up a chain if the logic is not really "chained"
response.then(data => console.log(data.data));
response.then(data => this.setState({ posts: data.data }, () => console.log(this.state.data)));
response.catch(err => console.log(err));
In short, I'm trying to plot markers on a map using react-native-maps.
I've gone as far as creating an action to fetch the coordinates and respective ID from the server (see code below).
export const getPlacesOnMap = () => {
return dispatch => {
dispatch(authGetToken())
.then(token => {
return fetch("myApp?auth=" + token);
})
.catch(() => {
alert("No valid token found!");
})
.then(res => {
if (res.ok) {
return res.json();
} else {
throw(new Error());
}
})
.then(parsedRes => {
const places = [];
for (let key in parsedRes) {
places.push({
// ...parsedRes[key], // this fetches all the data
latitude: parsedRes[key].location.latitude,
longitude: parsedRes[key].location.longitude,
id: key
});
} console.log(places)
dispatch(mapPlaces(places));
})
.catch(err => {
alert("Oops! Something went wrong, sorry! :/");
console.log(err);
});
};
};
export const mapPlaces = places => {
return {
type: MAP_PLACES,
places: places
};
};
I don't know if I'm using the right words, but I've essentially tested the code (above) using componentWillMount(), and it successfully returned multiple coordinates as an array of objects.
Now, the problem is I don't know what to do next. As much as I understand, I know the end goal is to create a setState(). But I don't know how to get there.
Would be a great help if someone can point me in the right direction.
You need to create an async action. You can dispatch different actions inside an async action based on whether the async function inside it is resolved or rejected.
export function getPlacesOnMap(token) {
return async function(dispatch) {
dispatch({
type: "FETCHING_PLACES_PENDING"
});
fetch("myApp?auth=" + token)
.then(res => {
dispatch({
type: "FETCHING_PLACES_FULFILLED",
payload: res.json()
});
})
.catch(error => {
dispatch({
type: "FETCHING_PLACES_REJECTED",
payload: error
});
});
};
}
If your authGetToken() function is also a promise, you need to dispatch this action after authGetToken() was resolved.
You can use the action.payload in your "FETCHING_PLACES_FULFILLED" case of your reducer(s) to be able to use the retrieved data.
UPDATE
Your reducer should be like this:
export default function reducer(
state = {
loadingMarkers : false,
markers : [],
error : null,
},
action
) {
switch (action.type) {
case "FETCHING_PLACES_PENDING":
return { ...state, loadingMarkers: true };
case "FETCHING_PLACES_FULFILLED":
return { ...state, loadingMarkers: false, markers: action.payload};
case "FETCHING_PLACES_REJECTED":
return { ...state, loadingMarkers: false, error: action.payload };
default:
return state;
}
}
Now you can connect your component to redux and use your markers when they are fetched.
have a look at this example and connect docs
I don't know if the title is the best one but, the problem is that I have a parent Component in React Native.
Inside this parent component, I fetch some location data (countryName, countryCode, etc).
This data is required for the children to fetch more data on its own to get specific info based on countryName. I just simply do an HTTP GET fetch() in the child.
Now, happens that whenever children's componentDidMount method is ran, I still don't have this.props.countryName available, therefore, my fetch inside the children Component fails to retrieve data.
I would like to somehow, whenever ParentComponent finishes fetching Location data, to notify ChildrenComponent so he can getAllReports() by countryName.
Here's my current code:
var ParentComponent = React.createClass({
getInitialState: function() {
return {
countryName: '',
regionName: '',
countryCode: ''
}
},
componentDidMount: function() {
this.getLocationData();
},
render: function() {
return (
<ChildComponent countryName={ this.state.countryName }></ChildComponent>
)
},
getLocationData() {
fetch(Constants.GEO_URL)
.then((response) => response.json())
.then((responseJson) => {
this.setState({
countryName: responseJson.country_name,
regionName: responseJson.region_name,
countryCode: responseJson.country_code
});
})
.catch((error) => {
console.log('Failed to get location data');
});
}
})
And this would be ChildComponent
var ChildComponent = React.createClass({
getInitialState: function() {
return {
reports: []
}
},
componentDidMount: function() {
this.getAllReports().catch((error) => { console.log("ERROR on getAllReports() - ") });
},
render: function() {
return (
<ScrollView refreshControl={
<RefreshControl
refreshing={ this.state.refreshing }
onRefresh={ this._onRefresh }
title={ i18n.t('loading') }
/>
}>
{ this.listReports() }
</ScrollView>
)
},
listReports: function() {
if (this.state.reports.length > 0) {
return this.state.reports.map(function(report, key) {
return <ReportItem key={ key } item={ report } />
});
}else{
return <Text style={ styles.statusMessage }>{ i18n.t('noMissingError') }</Text>
}
},
getAllReports: function() {
return new Promise((resolve, reject) => {
var url = Constants.API_LOCAL_URL + '/reports' + '?country=' + this.props.countryName;
console.log('URL > ' + url);
fetch(url)
.then((response) => response.json())
.then((responseJson) => {
console.log(responseJson)
this.setState({
reports: responseJson
});
resolve();
})
.catch((error) => {
alert('Error retrieving reports.');
reject();
})
});
}
})
As you may see, whenever ChildComponent is rendered, it doesn't have any countryCode, so it fails to retrieve. Also, i added the RefreshControl, so it actually retrieves data when i pull up on ScrollView, reloads all and get all the data... but just because I get the countryName afterwards.
I was looking for some way to do like trigger events from components and have a listener in others. Seems like there's no way to do that.
Any suggestion on how can I fix this?
I think a better solution maybe to fetch all of the data you need and then pass it down to the child component. That way the fetch is isolated to the parent, and you can guarantee its there before.
The beauty of the Promise is you can just keep going:
fetch(url)
.then(function(response) {
if (response.status >= 400) {
throw new Error("Bad response from server");
}
return response.json();
})
.then(function(data) {
// fetch here again if needed can also set state here or in next .then
that.setState({ person: data.person });
});