I'm trying to make a forecast app with React and Flux. I fetch the data from Yahoo Weather API, and put the data to my store with a callback in jsonp request.Then in the View, I get the data (in componentDidMount())from store as a state and pass some properties of it to child components.
The data(this.state.store), which is a Object, has two properties, called condition and forecast.The problem is that if I want to pass the this.state.store.condition(or forecast) to the child, it says TypeError: Cannot read property 'condition' of undefined. But if I just try to access this.state.store(for example, console.log(this.state.store)), there is no error.
Also, if I try to access this.state.store.condition in a try-catch statement, and log the error when there is one, I do access the condition successfully with the console printed TypeError above mentioned.
Here is my codes:
store:
const CHANGE_EVENT = 'change';
let _app = {};
// create a city
function create(city, data) {
_app[city.toUpperCase()] = {
condition: data.condition,
forecast: data.forecast,
};
}
const AppStore = Object.assign({}, EventEmitter.prototype, {
getAll() {
return _app;
},
emitChange() {
this.emit(CHANGE_EVENT);
},
addChangeListener(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
});
// register callback
AppDispatcher.register((action) => {
switch (action.actionType) {
case AppConstants.CREATE_CITY: {
create(action.city, action.data);
AppStore.emitChange();
break;
}
// other cases
default:
// noop
}
});
actions:
function callback(city, data) {
console.log(data);
const action = {
actionType: AppConstants.CREATE_CITY,
city,
data,
};
AppDispatcher.dispatch(action);
}
const AppActions = {
create(city) {
getDataFromAPI(city, callback);
},
};
utils:
function getDataFromAPI(query, callback) {
let data;
const url = `https://query.yahooapis.com/v1/public/yql?q=select * from weather.forecast where u='c' AND woeid in (select woeid from geo.places(1) where text="${query}")&format=json`;
superagent
.get(url)
.use(jsonp)
.end((err, res) => {
console.log(res.body.query.results.channel.item);
data = res.body.query.results.channel.item;
callback(query, data);
});
}
views:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
store: Store.getAll(),
currentCity: 'BEIJING',
};
this.onChange = this.onChange.bind(this);
this.getCurrentCity = this.getCurrentCity.bind(this);
}
componentWillMount() {
AppActions.create('BEIJING');
}
componentDidMount() {
Store.addChangeListener(this.onChange);
}
onChange() {
this.setState({ store: Store.getAll() });
}
getCurrentCity(city) {
this.setState({ currentCity: city.toUpperCase() });
}
componentWillUnmout() {
Store.removeChangeListener(this.onChange);
}
render() {
// For now, I have to do all of these to pass the condition to the child component
let condition;
let forecast;
let text;
let temp;
let currentWeatherCode;
let forecastWeatherCode = [];
let currentWeatherClassName;
let forecastWeatherClassName = [];
let date;
let forecastDate = [];
console.log(this.state.store[this.state.currentCity]);<--NO ERROR
// console.log(this.state.store[this.state.currentCity])<--UNDEFINED
// console.log(this.state.store[this.state.currentCity].condition);<--CANNOT READ PROPERTY
^
|
ERROR ON THIS 2 STATEMENTS
try {
condition = this.state.store[this.state.currentCity].condition;
forecast = this.state.store[this.state.currentCity].forecast;
text = condition.text.toUpperCase();
temp = condition.temp;
currentWeatherCode = condition.code;
currentWeatherClassName = setWeatherIcon(currentWeatherCode);
date = condition.date;
for (let i = 0; i < 6; i++) {
forecastWeatherCode.push(forecast[i].code);
forecastWeatherClassName.push(setWeatherIcon(forecastWeatherCode[i]));
forecastDate.push(forecast[i].date);
}
} catch (err) {
console.log(err);<--STILL ERROR, BUT I DO ACCESS THE PROP CONDITION IN THIS WAY
}
return (
<div>
<Today
city={this.state.currentCity}
weatherStatus={text}
tempreture={temp}
currentWeatherClassName={currentWeatherClassName}
date={date}
/>
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector('#app'));
It seems to me that you are trying to access the this.state.store[this.state.currentCity] property before it is fetched from the remote API.
You could add some sort of indication that the data is still being fetched like this.
render() {
// For now, I have to do all of these to pass the condition to the child component
let condition;
let forecast;
let text;
let temp;
let currentWeatherCode;
let forecastWeatherCode = [];
let currentWeatherClassName;
let forecastWeatherClassName = [];
let date;
let forecastDate = [];
console.log(this.state.store[this.state.currentCity]);<--NO ERROR
if (!this.state.store.hasOwnProperty(this.state.currentCity)) {
return <div>Loading...</div>;
}
... the rest of your original code
}
When it is done loading the setState() method is invoked and render() is called again. The second time it will fall trough the if and run your code.
Related
i'm pretty new at React so i need some help
i have an component which has structure smth like that
class App extends React.Component{
constructor(props) {
super(props);
this.state = {
...
portfolio: []
...
}
...
this.userDataUpdatesSubscribe = this.userDataUpdatesSubscribe.bind(this);
this.dealComplete = this.dealComplete.bind(this);
...
}
async componentDidMount() {
...
await this.userDataUpdatesSubscribe();
...
}
userDataUpdatesSubscribe(){
//fs here is a Firebase Firestore SDK
//firebase.initializeApp(firebaseConfig);
//const fs = firebase.firestore();
fs.collection('users').doc(...).onSnapshot(
e => {
this.portfolioInit(e.data().portfolio).then(v =>
this.setState({
portfolio: v,
...
})
);
}
);
}
async portfolioInit(stocks){
let a = [];
for (const v of stocks) {
let i;
await this.getPrice(v.ticker).then(e => {
i = e.trade.p
});
a.push({
ticker: v.ticker,
name: StocksData[v.ticker].name,
price: await i,
avgPrice: v.avgPrice,
count: v.count
});
}
return a;
}
async getPrice(ticker, date=null){
let a;
let u = ...;
await fetch(
u,
{
...
}
).catch(() => {...}).then(
async e => {
a = await e.json();
}
)
return await Promise.resolve(a);
}
render(){
<...>
<TheProblemComponent p={this.state.portfolio} .../>
</...>
}
}
so i have a component where state updates on firestore snapshot. i don't store prices in database but i need it so i use getPrice method which returns me price. when i've got all prices the state updates. then i convey data to which has structure like
const TheProblemComponent = (p) => {
const makeDeal() => {
let x = p.p;
...
some calculations
...
for(let i = 0; i < x.lenght; i++){
x[i] = {
ticker: x[i].ticker,
avgPrice: x[i].avgPrice,
count: x[i].count
} // so here i just delete price and name properties which are from props
}
fs.collection('users').doc(...).update({portfolio: x, ...}).then(() => {
...some actions
})
}
return <Button
onClick={() => {console.log(p.p); makeDeal()}}
></Button>
}
so again. i have a parent component which state updates on firestore snapshot. portfolio in database has avgPrice, count and ticker. in my component it also has price which i receive from the getPrice method and name which is constant. this state portfolio i send to ProblemComponent as props which should not modify the parent's state but it does. console.log() prints array without price and name even when executes before makeDeal function. i've tried to store price and name in DB but i want not to do it
It appears you are mutating the parent state via the passed prop because you are mutating the p prop.
const makeDeal() => {
let x = p.p; // <-- saved reference to p.p (portfolio??)
...
some calculations
...
for(let i = 0; i < x.lenght; i++){
x[i] = { // <-- mutation!!
ticker: x[i].ticker,
avgPrice: x[i].avgPrice,
count: x[i].count
}
}
fs.collection('users')
.doc(...)
.update({ portfolio: x, ... })
.then(() => {
...some actions
});
}
To resolve, shallow copy the data you want to update. I suggest also using more descriptive variable names so the code is more readable.
const makeDeal() => {
const portfolio = [...p.p]; // <-- copy array into new array reference
...
some calculations
...
for(let i = 0; i < portfolio.length; i++){
portfolio[i] = { // <-- update the copy
ticker: portfolio[i].ticker,
avgPrice: portfolio[i].avgPrice,
count: portfolio[i].count
}
}
fs.collection('users')
.doc(...)
.update({ portfolio, ... })
.then(() => {
...some actions
});
}
This same code is written for multiple pages with some changes and redux is implemented for state management. Please guide me I want to write simple and easy code.
class Grid extends Component {
constructor(props) {
super();
this.loadMoreRef = React.createRef();
this.state = {
loaded: true
};
}
componentDidMount() {
let p1 = new Promise((resolve, reject) => {
let { search_payload } = this.props;
let payload = { ...search_payload };
payload.statusId = this.props.statusId;
this.props.unitSearchAction({
type: "UNIT_SEARCH",
value: payload
});
resolve("");
});
loadMore() {
let unit_store = this.props.store || [];
let obj = unit_store[unit_store.length - 1];
let { search_payload } = this.props;
let payload = { ...search_payload };
if (payload.sortBy) {
if (payload.sortBy === "id") {
payload.lastKey = obj[payload.sortBy];
} else {
payload.lastKey = obj[payload.sortBy + 'Srt'];
}
The redux part is implemented differently for every component which has increased the code length so much.
I am trying to automatically scroll to the bottom of a div that contains a list of elements once a new element has been added.
Since adding and removing elements is done via Axios using an API, I have to wait for a response from the server in order to update my state in Vuex.
This means that, once an element is added in my state, every time I call the "scrollDown" function, the function scrolls to the second last element (due to the asynchronous Axios call not being registered yet).
My question is, how do I wait for the action in Vuex to finish and then call the function in my component to scroll to the bottom of my div?
I tried using watchers, computed properties, sending props, tracking changes from the actual state in Vuex and none of that worked...
// VUEX
const state = {
visitors: [],
url: 'API URL',
errors: []
}
const mutations = {
ADD_VISITOR(state, response) {
const data = response.data;
data.Photos = [];
state.visitors.data.push(data);
},
}
const actions = {
addVisitor: ({ commit }, insertion) => {
axios
.post(state.url + 'api/visitor', {
name: insertion.visitorName
})
.then(response => {
commit('ADD_VISITOR', response);
})
.catch(error => state.errors.push(error.response.data.message));
state.errors = [];
},
}
// MY COMPONENT FROM WHERE THE ACTIONS ARE BEING DISPATCHED
<div ref="scroll" class="visitors-scroll">
<ul v-if="visitors.data && visitors.data.length > 0" class="list-group visitors-panel">
<!-- Displaying appVisitor component and sending data as a prop -->
<app-visitor v-for="visitor in visitors.data" :key="visitor.id" :visitor="visitor"></app-visitor>
</ul>
</div>
methods: {
// Function that dispatches the "addVisitor" action to add a new visitor to the database
newVisitor() {
const insertion = {
visitorName: this.name
};
if (insertion.visitorName.trim() == "") {
this.errors.push("Enter a valid name!");
} else {
this.$store.dispatch("addVisitor", insertion);
this.name = "";
}
this.errors = [];
this.scrollDown(); // I WANT TO CALL THIS FUNCTION WHEN AXIOS CALL IS FINISHED AND MUTATION IN VUEX IS COMPLETED
},
scrollDown() {
this.$refs.scroll.scrollTop = this.$refs.scroll.scrollHeight;
}
},
Any help is appreciated!
In vuex dispatched action returns a Promise. In case of your code its empty Promise, because there is nothing to return. You need to return/pass your axios Promise and then wait for it in your component. Look at this fixed code:
// VUEX
const state = {
visitors: [],
url: 'API URL',
errors: []
}
const mutations = {
ADD_VISITOR(state, response) {
const data = response.data;
data.Photos = [];
state.visitors.data.push(data);
},
}
const actions = {
addVisitor: ({ commit }, insertion) => {
return axios
.post(state.url + 'api/visitor', {
name: insertion.visitorName
})
.then(response => {
commit('ADD_VISITOR', response);
})
.catch(error => state.errors.push(error.response.data.message));
state.errors = [];
},
}
// MY COMPONENT FROM WHERE THE ACTIONS ARE BEING DISPATCHED
<div ref="scroll" class="visitors-scroll">
<ul v-if="visitors.data && visitors.data.length > 0" class="list-group visitors-panel">
<!-- Displaying appVisitor component and sending data as a prop -->
<app-visitor v-for="visitor in visitors.data" :key="visitor.id" :visitor="visitor"></app-visitor>
</ul>
</div>
methods: {
// Function that dispatches the "addVisitor" action to add a new visitor to the database
newVisitor() {
const insertion = {
visitorName: this.name
};
if (insertion.visitorName.trim() == "") {
this.errors.push("Enter a valid name!");
} else {
this.$store.dispatch("addVisitor", insertion)
.then(() => {
this.scrollDown();
})
this.name = "";
}
this.errors = [];
},
scrollDown() {
this.$refs.scroll.scrollTop = this.$refs.scroll.scrollHeight;
}
},
You could try using the async/await syntax.
This means when it will wait until this.$store.dispatch("addVisitor", insertion) is resolved, that means until response from the API is there, the next lines of code will not be executed.
methods: {
// Function that dispatches the "addVisitor" action to add a new visitor to the database
async newVisitor() {
const insertion = {
visitorName: this.name
};
if (insertion.visitorName.trim() == "") {
this.errors.push("Enter a valid name!");
} else {
await this.$store.dispatch("addVisitor", insertion);
this.name = "";
}
this.errors = [];
this.scrollDown();
},
scrollDown() {
this.$refs.scroll.scrollTop = this.$refs.scroll.scrollHeight;
}
}
Edit: And in your Vueux action, make sure to add a return statement.
const actions = {
addVisitor: ({ commit }, insertion) => {
return axios
.post(state.url + 'api/visitor', {
name: insertion.visitorName
})
.then(response => {
commit('ADD_VISITOR', response);
})
.catch(error => state.errors.push(error.response.data.message));
state.errors = [];
},
}
I am getting data from the backend to display it in the font like this
componentDidMount() {
const response = this.props.store.privateImputationData;
console.log(response);
}
It displays null in the console, now if i do a setTimeout it works!
componentDidMount() {
setTimeOut(() => {
const response = this.props.store.privateImputationData;
console.log(response);
}, 500);
}
This how i m getting data from my store:
#computed get ImputationData() {
return this.privateImputationData || {};
}
loadImputation = (diplayedImputations) => {
HttpClient.postJSON(this.apiDataUrl, diplayedImputations).then((result) => {
this.privateImputationData = result;
this.loadAdditionalData();
});
}
How can i do it without setTimeout?
You can use the state object: State and Lifecycle. Whenever the state changes, whatever component uses it, get's updated too.
this.state = {privateImputationData: null} //or some default
So in your code:
#computed get ImputationData() {
return this.privateImputationData || {};
}
loadImputation = (diplayedImputations) => {
HttpClient.postJSON(this.apiDataUrl, diplayedImputations).then((result) => {
this.setState({privateImputationData: result});
this.loadAdditionalData();
});
}
To use the value:
this.state.privateImputationData;
I'm doing a project that fetch different types of data from SWAPI API (people, planets, etc.) using react but I have an issue with multiple Ajax request.
The problem is when I quickly request from 2 different URL for example, 'species' and 'people', and my last request is 'species' but the load time of 'people' is longer, I will get 'people' instead.
What I want is to get the data of the last clicked request, if that make sense.
How do I achieve that? All the solution I found from Google is using jQuery.
Here's a slice of my code in src/app.js (root element) :
constructor(){
super();
this.state = {
searchfield: '',
data: [],
active: 'people'
}
}
componentDidMount() {
this.getData();
}
componentDidUpdate(prevProps, prevState) {
if(this.state.active !== prevState.active) {
this.getData();
}
}
getData = async function() {
console.log(this.state.active);
this.setState({ data: [] });
let resp = await fetch(`https://swapi.co/api/${this.state.active}/`);
let data = await resp.json();
let results = data.results;
if(data.next !== null) {
do {
let nextResp = await fetch(data.next);
data = await nextResp.json();
let nextResults = data.results
results.push(nextResults);
results = results.reduce(function (a, b) { return a.concat(b) }, []);
} while (data.next);
}
this.setState({ data: results});
}
categoryChange = (e) => {
this.setState({ active: e.target.getAttribute('data-category') });
}
render() {
return (
<Header searchChange={this.searchChange} categoryChange={this.categoryChange}/>
);
}
I made a gif of the problem here.
Sorry for the bad formatting, I'm writing this on my phone.
You have to store your requests somewhere and to abandon old ones by making only one request active. Something like:
getData = async function() {
console.log(this.state.active);
this.setState({ data: [] });
// my code starts here
if (this.controller) { controller.abort() }
this.controller = new AbortController();
var signal = controller.signal;
let resp = await fetch(`https://swapi.co/api/${this.state.active}/`, { signal });
let data = await resp.json();
let results = data.results;
if(data.next !== null) {
do {
let nextResp = await fetch(data.next);
data = await nextResp.json();
let nextResults = data.results
results.push(nextResults);
results = results.reduce(function (a, b) { return a.concat(b) }, []);
} while (data.next);
}
this.setState({ data: results});
}