Reputation: 75
I have a massive problem with my weather app.
I'm using the OpenweatherMap free API and I created a page with two select inputs: one for choosing a country from an array of objects stated directly within the state and second select for choosing a city.
The second select is tricky since I uploaded a huge JSON file containing a list of all cities served by the API and I want to filter out every city which does not belong to the previously chosen country.
My issue is that the generateCitiesList()
function doesn't pass filtered elements into the this.state.cityList
import React from "react";
import ReactDOM from "react-dom";
import CITIES from "./cities.json";
import COUNTRIES from "./countries.json";
import "./styles.css";
// So getCitiesByCountry accepts (country) parameter for which we use
// this.state.selectedCountry and filter how exactly?
// Do we compare every element within cities.json file so that it returns
// an object (array?) that meets the criteria of element.country being equal to
// this.state.selectedCountry?
const getCitiesByCountry = (country) =>
CITIES.filter(city => city.country === country);
//
class App extends React.PureComponent {
state = { selectedCoutnry: "" };
handleOnSelectCountry = e => {
this.setState({ selectedCoutnry: e.target.value });
};
render() {
// What does this line of code do? It looks like an object destructurization
// so should I assume that from now on
// const selectedCountry is equal to this.state.selectedCountry ?
const { selectedCoutnry } = this.state;
return (
<div className="App">
<select value={selectedCoutnry} onChange={this.handleOnSelectCountry}>
<option>Select a Country</option>
// That makes a lot of sense. Why would I store countries list in state
while I could access it via JSON file.
{COUNTRIES.map(({ name, id }) => (
<option value={id} key={id}>
{name}
</option>
))}
</select>
{selectedCoutnry && (
<select>
<option>Select a City</option>
// So we use a function getCitiesByCountry which returns a filtered object
// which is then mapped through to render <option>s?
{getCitiesByCountry(selectedCoutnry).map(({ name, id }) => (
<option value={id} key={id}>
{name}
</option>
))}
</select>
)}
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
[ { "id": 707860, "name": "Hurzuf", "country": "UA", "coord": { "lon": 34.283333, "lat": 44.549999 } }, { "id": 519188, "name": "Novinki", "country": "RU", "coord": { "lon": 37.666668, "lat": 55.683334 } }, { "id": 1283378, "name": "Gorkhā", "country": "NP", "coord": { "lon": 84.633331, "lat": 28 } } ]
Upvotes: 1
Views: 485
Reputation: 75
Ok, thanks to @Yoav Kadosh (kudos!) I have the answear below.
It turns out that in order to effectively map through a selected number of items you should:
Instead, you can also apply chain operators such as variable.filter( (parameter) => parameter > 2).map({element} => {element} ).
constructor(props) {
super(props);
this.state = {
country: '',
city: '',
result: {
temperature: ''
}
}
}
handleCountrySelect = (e) => {
this.setState({
country: e.target.value
});
console.log(e.target.value);
};
filterCitiesByCountry = (country) => {
return citiesJSON.filter ( city => city.country === country)
};
handleCitySelect = (e) => {
this.setState ({
city: e.target.value
});
console.log(e.target.value)
};
getWeather = async (e) => {
e.preventDefault();
fetch(`http://api.openweathermap.org/data/2.5/weather?q=${this.state.city},${this.state.country}&appid=${APIKEY}`)
.then( res => res.json())
.then( res =>
this.setState ({
result: {
temperature: Math.round(res.main.temp - 273.15)
}
})
)
};
render() {
return (
<main>
<section className='row text-center' id='weather_select_section'>
<form>
<div className="countrySelect col-12 col-sm-6 text-center mt-5 mb-5 ">
<label> Select Country </label>
<select onChange={this.handleCountrySelect} required placeholder='Country...'>
<option value='' data-index='Country...'> </option>
{ countriesJSON.map(({name, code, key} ) => {
return (
<option value={code} key={key} > {name} </option>
)
})}
</select>
</div>
<div className="citySelect col-12 col-sm-6 text-center mt-5 mb-5 ">
<label> Select City </label>
{this.state.country && (
<select onChange={this.handleCitySelect} placeholder='City...' required>
<option value='' data-index='City...'></option>
{ this.filterCitiesByCountry(this.state.country).map(({name, key}) =>
<option value={name} key={key}> {name} </option>
)}
</select>
)
}
</div>
<button type='submit' onClick = {this.getWeather} className=" col-10 mt-5 mb-5 text-center "> Get Weather</button>
</form>
<div className=" col-12 WeatherInfoBox ">
<h3> Weather in {this.state.city}</h3>
<p> {this.state.result.temperature} ℃</p>
Upvotes: 0
Reputation: 5155
Your code didn't work because of this:
citiesArray.push(filterCities)
which should've been written as:
citiesArray.push(...filterCities)
But you can avoid all that by reducing your code to:
generateCitiesList = () => {
this.setState ({
listCity: citiesList.filter(({country}) => (
country === `${this.state.country}`;
))
});
};
However, you should only use the state for storing the minimum data that affects the view, in your case - the selected country/city.
The list of cities for the currently selected country should not be part of the state since it can be derived from the selected country.
Simply write a function that takes a country and return the list of cities for that country, and run it in the render()
against the selected country:
render() {
return (
...
<select>
{getCitiesByCountry(this.state.country).map(city => (
<option value={city}>{city}</option>
))}
</select>
...
);
}
Upvotes: 0
Reputation: 8982
This looks evil. Don't mutate the state directly.
generateCitiesList = () => {
const self = this;
self.state.cityList.push(citiesList.filter( function (countries) {
return countries.country === `${self.state.country}`
}));
console.log(this.state.cityList);
this.showCitiesList() ===> WORKS, pushing desired objects into an array
};
Populate the state instead in the lifecycle function componentDidMount
componentDidMount() {
const cityList = citiesList.filter(element => element.country === this.state.country);
this.setState({cityList});
}
Upvotes: 0
Reputation: 31485
See if that works for you.
This is representing your JSON with the cities.
cities: {
city1: {
cityName: 'City1',
country: 'A'
},
city2: {
cityName: 'City2',
country: 'A'
},
city3: {
cityName: 'City3',
country: 'B'
},
city4: {
cityName: 'City4',
country: 'B'
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state={
selectedCountry: 'A',
countries: ['A','B'],
cities: {
city1: {cityName: 'City1',
country: 'A'},
city2: {cityName: 'City2',
country: 'A'},
city3: {cityName: 'City3',
country: 'B'},
city4: {cityName: 'City4',
country: 'B'},
}
}
this.selectCountry = this.selectCountry.bind(this);
}
selectCountry() {
this.setState((prevState) => {
return({
...prevState,
selectedCountry: prevState.selectedCountry === 'A' ? 'B' : 'A'
});
});
}
render() {
let filteredCities = [];
for (let i in this.state.cities) {
this.state.selectedCountry === this.state.cities[i].country ?
filteredCities.push(this.state.cities[i]) : null;
}
filteredCities = filteredCities.map((item)=>
<li>{item.cityName}</li>
);
return(
<React.Fragment>
<div>Selected Country: {this.state.selectedCountry}</div>
<button onClick={this.selectCountry}>Change Country</button>
<div>Country List</div>
<div>
<ul>
{filteredCities}
</ul>
</div>
</React.Fragment>
);
}
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Upvotes: 0