I am using react/redux to build a simple weather retrieval page from a weather api. My question is about handling conditionals in react with a redux state manager. I have a table that displays weather information when a city is entered. The header for this table is visible before the query is made. I want to add a conditional to replace the whole table with a div that tells the user to search for a city. When a search is made I want to render the table.
I made a conditional inside the render method of the list weather container so if there is no weather data display the div else display the table. My problem is the table never displays only the div.
This confuses me because a search should update the state of the container and re-render it with the data, no? I'm guessing that I need to add this logic in my reducer or state? I'm not sure which to use or where to place the conditional to render a div if there is data or the table if a search has been made and there is data.
// search container
class SearchBar extends Component {
constructor(props) {
super(props);
this.state = { term: '' };
this.onInputChange = this.onInputChange.bind(this);
this.onFormSubmit = this.onFormSubmit.bind(this);
}
onInputChange(event) {
this.setState({ term: event.target.value });
}
onFormSubmit(event) {
event.preventDefault();
this.props.fetchWeather(this.state.term);
this.setState({ term : ''});
}
render() {
return (
<form onSubmit={this.onFormSubmit}
className={styles.search}>
<div className={styles.wrap}>
<input value={ this.state.term }
onChange={ this.onInputChange }
placeholder="Enter US cities for a forecast"
className={styles.wrap__input}/>
<button type="submit" className={styles.wrap__btn}>search</button>
</div>
</form>
)
}
}
function MapDispatchToProps(dispatch) {
return bindActionCreators({ fetchWeather }, dispatch);
}
export default connect(null, MapDispatchToProps)(SearchBar);
// weather list container
class ListWeather extends Component {
renderWeather(data){
if(!data) {
let id = Math.round(Math.random() * 100);
return (
<tr key={ id }>
<td className={styles.table__data_error} colspan="5">Please enter a valid US City.</td>
</tr>
)
} else {
const temps = data.list.map(weather => weather.main.temp * 9/5 - 459.67);
const pressures = data.list.map(weather => weather.main.pressure);
const humidities = data.list.map(weather => weather.main.humidity);
const avg = "AVG";
return (
<tr key={ data.city.id }>
<td className={styles.table__data_name}>{ data.city.name }</td>
<td><Chart color="red" data={temps} />{avg} Temperature</td>
<td><Chart color="green" data={humidities} />{avg} Humidity</td>
<td><Chart color="blue" data={pressures} />{avg} Pressure</td>
</tr>
)
}
}
render(){
if(!this.props.weather.data) {
return (
<div className={styles.start}>Enter a US City to get started.</div>
)
} else {
return (
<table className={styles.table}>
<thead className={styles.table__head}>
<tr className={styles.table__row}>
<th className={styles.table__rowhead}>City</th>
<th className={styles.table__rowhead}>Temp</th>
<th className={styles.table__rowhead}>Humidity</th>
<th className={styles.table__rowhead}>Pressure</th>
</tr>
</thead>
<tbody className={styles.table__body}>
{ this.props.weather.map(this.renderWeather)}
</tbody>
</table>
);
}
}
}
function MapStateToProps({ weather }) {
return { weather };
}
export default connect(MapStateToProps)(ListWeather)
// actions / index.js
import axios from 'axios';
const API_KEY='b60aa70986cac3edb4248b5569b74a92';
const ROOT_URL=`http://api.openweathermap.org/data/2.5/forecast?
appid=${API_KEY}`;
export const FETCH_WEATHER = 'FETCH_WEATHER';
export function fetchWeather(city) {
const url = `${ROOT_URL}&q=${city},us`;
const request = axios.get(url);
return {
type: FETCH_WEATHER,
payload: request
};
}
// reducers/reducer_weather.js
import { FETCH_WEATHER } from '../actions/index';
export default function(state=[], action) {
switch (action.type) {
case FETCH_WEATHER:
// return state.concat([ action.payload.data ]);
return [ action.payload.data, ...state ]
default : return state;
}
return state;
}
//reducers/index.js
import { combineReducers } from 'redux';
import WeatherReducer from './reducer_weather';
const rootReducer = combineReducers({
weather: WeatherReducer
});
export default rootReducer;
Change your check to check if this.props.weather has a length > 0. Looks like this.props.weather.data will never exist since this.props.weather is an array:
render(){
if(!this.props.weather.length) {
return (
<div className={styles.start}>Enter a US City to get started.</div>
)
} else {
return (
<table className={styles.table}>
<thead className={styles.table__head}>
<tr className={styles.table__row}>
<th className={styles.table__rowhead}>City</th>
<th className={styles.table__rowhead}>Temp</th>
<th className={styles.table__rowhead}>Humidity</th>
<th className={styles.table__rowhead}>Pressure</th>
</tr>
</thead>
<tbody className={styles.table__body}>
{ this.props.weather.map(this.renderWeather)}
</tbody>
</table>
);
}
}
Related
Trying to display a filtered table, but unsuccessful, see below.
Keep getting an undefined error when reading map, can't seem to figure out why colleges aren't being passed when calling on filteredColleges.map(college)
Could this possibly be because I'm calling on the <Table/> component inside my App.js and forgot a basic step?
App.js looks like so:
return (
<div className="App">
<header className="App-header">
<h1>Partners</h1>
<Table/>
</header>
</div>
);
}
If anyone could help I'd be very, very grateful. Thanks in advance!
import React , {Component } from 'react'
export default class Table extends Component {
// Display API data - componentDidMount - pull specific elements - DONE
// Create table displaying API data
// Create Search bar
// Create drop down list for prefix
// Style Ofsted Rating column
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
colleges: [],
searchByName: ""
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({ searchByName: event.target.value });
}
componentDidMount() {
fetch(`https://collegesapi/data.json`)
.then(res => res.json())
.then(
(result) => {
this.setState({
isLoaded: true,
colleges: result.getColleges
});
},
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
renderRow(college) {
return (
<tr key={college.id}>
<td>{college.name}</td>
<td>{college.groupPrefix}</td>
<td>
{college.logo && (
<img className="w-full" src={college.logo} alt={college.name} />
)}
</td>
<td>{college.ofstedRating}</td>
</tr>
)
}
render() {
const filterColleges = (colleges, query) => {
if(!query) {
return colleges;
}
return colleges.filter((college) => {
const collegeName = college.name.toLowerCase();
return collegeName.includes(query);
});
};
const filteredColleges = filterColleges(
this.props.colleges,
this.state.searchByName
);
const { error, isLoaded, colleges } = this.state;
if (error) {
return <div>Error: {error.message}</div>
} else if (!isLoaded) {
return <div>Loading...</div>;
} else {
return (
<div>
<table className="table-fixed border-collapse">
<thead>
<tr>
<input
type="text"
placeholder="Search partners"
onChange={this.handleChange}
/>
</tr>
<tr>
<th className="w-1/12">Partner</th>
<th className="w-1/12">Prefix</th>
<th className="w-1/12">Logo</th>
<th className="w-1/12">Ofsted Rating</th>
</tr>
</thead>
<tbody>{filteredColleges.map((college) => this.renderRow(college))}</tbody>
</table>
</div>
)
}
}
}
You are referring this.props.colleges while sending to "filterColleges" function:
const filteredColleges = filterColleges(
this.props.colleges,
this.state.searchByName
);
Instead, since you are setting it in states when received from API response, you should refer accordingly:
const filteredColleges = filterColleges(
this.state.colleges,
this.state.searchByName
);
Also, it is always good to handle error or empty responses, for example, if your API does not return even empty array, then this code will still throw the same error. So you can add null coalescing checks (??) and assign empty array in these cases, as shown below:
this.setState({
isLoaded: true,
colleges: result?.getColleges ?? [],
});
Hope it helps!
I've come to a halt making this covid19 app where I can see a list of countries on the left side of the screen with the option of adding any number of countries to the right side of the screen, which displays more covid data of the added country. I'm also kinda new to React.
Problem is, when I click the add button the added state is updated, and it displays that added country on the right side of the screen. But, when I try adding another country I get an error. I believe the error is somewhere around when I try to setState({ state }) in the addCountry method from within App.js.
In other words, the 'added' state is only letting itself hold no more than one array element. Help much much much appreciated. I posted all the code.
index.js
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
App.js
import CountryList from "./components/CountryList.js";
import Find from "./components/Find.js";
import Added from "./components/Added.js";
class App extends Component {
constructor() {
super();
this.state = {
countries: [],
inputbox: [],
added: [],
};
}
// Arrow functions capture "this" when they are defined, while standard functions do when they are executed.
// Thus, no need for the bind method. Awesome.
handleChange = (e) =>
this.setState({
inputbox: e.target.value,
});
getCountryData = async (slug) => {
const resp = await fetch(`https://api.covid19api.com/live/country/${slug}`);
var addedData = await resp.json();
// Api returns most days of covid, per country, that it tracks
// Thus, we want the last tracked day of a country
addedData = addedData[addedData.length - 1];
return addedData;
};
// Add a country to the added state
// Call when user clicks button associated with their desired country
addCountry = async (btnId) => {
const { countries, added } = this.state;
var addedData = await this.getCountryData(btnId);
countries.map((country) => {
// If the button ID is equal to the current country in the loops' Slug
if (btnId == country.Slug) {
try {
added.push([
{
addedCountry: addedData.Country,
confirmedTotal: addedData.Confirmed,
deathsTotal: addedData.Deaths,
recoveredTotal: addedData.Recovered,
activeTotal: addedData.Active,
},
]);
// (bug) IT IS PUSHING, BUT ITS NOT SETTING THE STATE!
// ITS ONLY LETTING ME KEEP ONE ITEM IN THE STATE
this.setState({ added });
console.log(added);
} catch (error) {
alert(`Sorry, country data not available for ${country.Country}`);
return;
}
}
});
};
removeCountry = (btnId) => {
const { added } = this.state;
added.map((added, index) => {
//console.log(added[index].addedCountry);
if (btnId == added[index].addedCountry) {
added.splice(index, 1);
this.setState({ added: added });
} else {
console.log("not removed");
return;
}
});
};
// Mount-on lifecycle method
async componentDidMount() {
const resp = await fetch("https://api.covid19api.com/countries");
const countries = await resp.json(); // parsed response
this.setState({ countries }); // set state to parsed response
}
render() {
// Filter out countries depending on what state the inputbox is in
const { countries, inputbox } = this.state;
const filtered = countries.filter((country) =>
country.Country.includes(inputbox)
);
return (
<div className="App Container">
<Find
placeholder="Type to find a country of interest..."
handleChange={this.handleChange}
/>
<div className="row">
<CountryList countries={filtered} addCountry={this.addCountry} />
<Added added={this.state.added} removeCountry={this.removeCountry} />
</div>
</div>
);
}
}
export default App;
Added.js
import React, { Component } from "react";
import { Table, Form, Input, Button } from "reactstrap";
import AddedCountry from "./AddedCountry.js";
class Added extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="col-md-6">
<Table>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Country</th>
<th scope="col">Active</th>
<th scope="col">Confirmed Total</th>
<th scope="col">Recovered</th>
<th scope="col">Deaths</th>
<th scope="col">Action</th>
</tr>
</thead>
{this.props.added.map((added, index) => (
<AddedCountry
added={added[index]}
removeCountry={this.props.removeCountry}
/>
))}
</Table>
</div>
);
}
}
export default Added;
AddedCountry.js
import React, { Component } from "react";
import { Table, Form, Input, Button } from "reactstrap";
class AddedCountry extends Component {
constructor(props) {
super(props);
}
render() {
return (
<tbody>
<tr>
<td></td>
<td>{this.props.added.addedCountry}</td>
<td>{this.props.added.activeTotal}</td>
<td>{this.props.added.confirmedTotal}</td>
<td>{this.props.added.recoveredTotal}</td>
<td>{this.props.added.deathsTotal}</td>
<td>
{
<Button
onClick={() =>
this.props.removeCountry(
document.getElementById(this.props.added.addedCountry).id
)
}
id={this.props.added.addedCountry}
type="submit"
color="danger"
size="sm"
>
Remove
</Button>
}
</td>
</tr>
</tbody>
);
}
}
export default AddedCountry;
CountryList.js
import React, { Component } from "react";
import { Table, Form, Input, Button } from "reactstrap";
import Country from "./Country.js";
class CountryList extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="col-md-6">
<Table>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Country</th>
<th scope="col">Actions</th>
</tr>
</thead>
{
// Each country is a component
// Function will display all countries as the Map function loops through them
this.props.countries.map((country) => (
<Country countries={country} addCountry={this.props.addCountry} />
))
}
</Table>
</div>
);
}
}
export default CountryList;
Country.js
import React, { Component } from "react";
import { Table, Form, Input, Button } from "reactstrap";
class Country extends Component {
constructor(props) {
super(props);
}
render() {
return (
<tbody>
<tr>
<td></td>
<td>{this.props.countries.Country}</td>
<td>
{
<Button
onClick={() =>
this.props.addCountry(
document.getElementById(this.props.countries.Slug).id
)
}
id={this.props.countries.Slug}
type="submit"
color="success"
size="sm"
>
Add
</Button>
}
</td>
</tr>
</tbody>
);
}
}
export default Country;
Find.js
import React, { Component } from "react";
import { Table, Form, Input, Button } from "reactstrap";
class Find extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="Find container">
<br />
<Form>
<div className="form-row">
<div className="form-group col-md-6">
<h3>Find a Country</h3>
<Input
type="text"
className="form-control"
id="country"
placeholder={this.props.placeholder}
onChange={this.props.handleChange}
></Input>
</div>
</div>
</Form>
</div>
);
}
}
export default Find;
I haven't pored over all that code, but focusing right where you think the issue is it is obvious you are mutating your state object by pushing directly into the added array.
Solution
Don't mutate state!
Since it seems you only want to add a single new "add" and only when the button's btnId matches a country's slug, and the btnId can only ever be a valid value from the mapped countries array, I think this can be greatly simplified.
addCountry = async (btnId) => {
const addedData = await this.getCountryData(btnId);
if (addedData) {
this.setState(prevState => ({
added: prevState.added.concat({ // <-- concat creates a new array reference
addedCountry: addedData.Country,
confirmedTotal: addedData.Confirmed,
deathsTotal: addedData.Deaths,
recoveredTotal: addedData.Recovered,
activeTotal: addedData.Active,
}),
}));
} else {
alert(`Sorry, country data not available for ${country.Country}`);
}
};
Similarly the removeCountry handler is mis-using the array mapping function and mutating the added state. Array.prototype.filter is the idiomatic way to remove an element from an array and return the new array reference.
removeCountry = (btnId) => {
this.setState(prevState => ({
added: prevState.added.filter(el => el.addedCountry !== btnId),
}));
};
Additional Issues & Suggestions
Added.js
If you maintain the added array as a flat array (not an array of arrays) then it's trivial to map the values.
{this.props.added.map((added) => (
<AddedCountry
key={added}
added={added}
removeCountry={this.props.removeCountry}
/>
))}
Country.js & AddedCountry.js
I don't see any reason to query the DOM for the button id when you are literally right there and can enclose the country slug in the onClick callback.
<Button
onClick={() => this.props.addCountry(this.props.countries.Slug)}
id={this.props.countries.Slug}
type="submit"
color="success"
size="sm"
>
Add
</Button>
<Button
onClick={() => this.props.removeCountry(this.props.added.addedCountry)}
id={this.props.added.addedCountry}
type="submit"
color="danger"
size="sm"
>
Remove
</Button>
App.js
This may or may not matter, but it is often the case to do case-insensitive search/filtering of data. This is to ensure something like "France" still matching a user's search input of "france".
const filtered = countries.filter((country) =>
country.Country.toLowerCase().includes(inputbox.toLowerCase())
);
I am having problem with sorting table column in React. My table composes of three components: one defining the individual row (row.js), one rendering headers and mapping the rows that need own states (rows.js) and finally table.js that renders the whole thing. The data of the table comes from the database.
Here is a part of the row.js:
class ProjectTableProjectRow extends Component {
render() {
const { project } = this.props;
return (
<tr>
<td className="projects">
<Body2>
<Link to={`/projects/${project.id}`}>{project.description}</Link>
</Body2>
</td>
export default withRouter(ProjectTableProjectRow);
And here an excerpt from my rows.js:
class ProjectTableProjectRows extends Component {
componentDidMount() {
this.props.projects.getAll(); // This gets all the projects from the store
}
onSortProjects = () => {
let sortedToBe = this.props.projects.list.map(project => project.description);
const sorted = sortedToBe.sort();
};
render() {
return (
<table>
<thead>
<tr>
<th>
<Caption>Project</Caption>
<IconButton onClick={() => this.onSortProjects()}>
<RowsIcon />
</IconButton>
</th>
</tr>
</thead>
{this.props.projects.list.map(project => (
<tbody key={project.id}>
<ProjectTableProjectRow project={project} />
</tbody>
))}
</table>
);
}
}
export default ProjectTableProjectRows;
Finally, there is a projectTable.js (that I am not sure if I need anyway...)
class ProjectTable extends Component {
render() {
return (
<>
<ProjectTableProjectRows projects={this.props.projects} />
</>
);
}
}
export default ProjectTable;
So, I'd like to sort the project.description column (there are project names as strings) in alphabetical order. Naturally the icon and sort function onClick in it is not connected to the table column, so the sort function does nothing to the table. How can I achieve this? I do not know yet how to think "in React".
This is obviously not tested, I'm just making a couple of updates to the code you had but this will allow you to use the sorted values.
class ProjectTableProjectRows extends Component {
constructor(props) {
super(props)
this.state = {
projects: null
}
}
componentDidMount() {
const projects = this.props.projects.getAll();
this.setState({ projects })
}
onSortProjects = () => {
let sortedToBe = this.state.projects.list.map(project => project.description);
const sorted = sortedToBe.sort();
this.setState({ projects: sorted });
};
render() {
return (
<table>
<thead>
<tr>
<th>
<Caption>Project</Caption>
<IconButton onClick={() => this.onSortProjects()}>
<RowsIcon />
</IconButton>
</th>
</tr>
</thead>
{this.state.projects && this.state.projects.list.map(project => (
<tbody key={project.id}>
<ProjectTableProjectRow project={project} />
</tbody>
))}
</table>
);
}
}
export default ProjectTableProjectRows;
I can get all props passed in React class in its render method.
But if I try to do the same in componentDidMount() or even in constructor() - no luck.
When debugging I can see props in
componentDidMount(){
const { users } = this.props <-props are already there
console.log('users from did mount', users); <-- undefined here
}
and nothing appears in users variable.
When the same code fires in render() method -- all is working fine.
upd: full class
import React, { Component } from 'react'
import { firebaseDB, functions } from '../firebase'
import map from 'lodash/map'
export default class Users extends Component {
state = {
users: []
}
componentDidMount(){
const { users } = this.props
console.log('users from did mount', users); // undefined here
// here I'd like to some work with those props
}
handleUser = user => () => {
// console.log('user to handle:',user); //is here
... handling user data
}
render() {
const { users } = this.props //ok
console.log('users from render', users);//see it
return (
<div className="container">
{users ?
<div>
<h4 className="text-primary">Users List</h4>
<table className="table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">email</th>
<th scope="col">Something</th>
</tr>
</thead>
<tbody>
{
map(users, (user, key) => (
<tr key={key}>
<td>{user.displayName}</td>
<td>{user.email}</td>
<td><button
className="btn btn-primary"
onClick={this.handleUser(user)}
>ok</button></td>
</tr>
))
}
</tbody>
</table>
</div>
:
"There are no users yet "
}
</div>
)
}
}
users are firebase object, comes from parent above
{ id1: {...}, id2: {...} }
You need to use componentWillReceiveProps method in case if users prop is coming from async
componentWillReceiveProps(nextProps){
if(this.props.users !== nextProps.users){
console.log('should appear here', nextProps.users);
}
}
I get data from github api
I have all the data i need to display, but I want to splice it so that i only get 20 repositories per page.
And I don't want a framework or a plugin for that.
I'm fairly new to React and JS in general so I don't know where to start or what to do next to create a pagination.
import React, {Component} from 'react';
import axios from 'axios';
class Apirequest extends Component {
constructor(){
super();
this.state = {
githubData: [],
};
}
componentDidMount() {
axios.get('https://api.github.com/search/repositories?q=language:javascript&sort=stars&order=desc&per_page=100')
.then(res => {
console.log('res', res)
this.setState({ githubData: res.data.items})
})
}
render() {
const { githubData } = this.state
return(
<div className="container">
{githubData.map((name, index) =>
<table key={name.id}>
<tr>
<th><img src={name.owner.avatar_url}/></th>
<td>{name.owner.login}<div className="border-bottom"></div></td>
<td>{name.description}<div className="border-bottom"></div></td>
<td><a href={name.homepage}>{name.homepage}</a></td>
</tr>
</table>
)}
</div>
)
}
}
export default Apirequest;
First of all your map function has a wrong logic. You are creating a table for each record and you should only create a row for each record. table tags should be outside of map.
render() {
const { githubData } = this.state
return(
<div className="container">
<table key={name.id}>
{githubData.map((name, index) =>
<tr>
<th><img src={name.owner.avatar_url}/></th>
<td>{name.owner.login}<div className="border-bottom"></div></td>
<td>{name.description}<div className="border-bottom"></div></td>
<td><a href={name.homepage}>{name.homepage}</a></td>
</tr>
)}
</table>
</div>
)
}
For pagination what you can do is to limit the number of rows you show by using Array.prototype.slice(). Just to give you an idea I am posting a small example. You might need to implement some more for this logic to work on your code.
Example
previousPage = () => {
if (this.state.currentPage !== 1)
this.setState((prevState) => ({currentPage: (prevState.currentPage - 1)}))
}
nextPage = () => {
if (this.state.currentPage + 1 < this.state.githubData.lenght)
this.setState((prevState) => ({currentPage: (prevState.currentPage + 1)}))
}
render() {
const { githubData, currentPage } = this.state
return(
<div className="container">
<table key={name.id}>
{githubData.slice((currentPage * 20), 20).map((name, index) =>
<tr>
<th><img src={name.owner.avatar_url}/></th>
<td>{name.owner.login}<div className="border-bottom"></div></td>
<td>{name.description}<div className="border-bottom"></div></td>
<td><a href={name.homepage}>{name.homepage}</a></td>
</tr>
)}
</table>
<button onClick={this.previousPage}>Previous Page</button>
<button onClick={this.nextPage}>Next Page</button>
</div>
)
}
set your state to have pagination info and data.
such as
state = {
pagination: {
start: 0,
rows: 20
},
githubData: ....
}
and now in you render function you can splice based on the pagination info. Anytime new page is clicked, you can set state to new start variable