I'm new to react and I'm stuck again. I'm trying to map my array to create new array of objects inside of my child component. Here's my issue - my method componentDidMount gets executed before data came from parents props, and my state stays empty. When I'm console.loging this.props and the end of componentDidMount I receive empty array, but when I'm console.loging it on render method it gives me 4 empty arrays, then it fills in to expected 300. What I'm doing wrong?
Parent component:
import "./App.css";
import { CompanyList } from "./components/companylist/companylist.component";
import { Searchfield } from "./components/searchfield/searchfield.component";
class App extends Component {
constructor(props) {
super(props);
this.state = {
companies: [],
searchfield: "",
};
}
componentDidMount = () => {
const URL = "https://xxxxx/companies";
fetch(URL)
.then((response) => response.json())
.then((data) => this.setState({ companies: data }))
.catch((error) => {
console.error("Error", error);
});
};
render() {
const filteredCompanies = this.state.companies.filter((item) =>
item.name.toLowerCase().includes(this.state.searchfield.toLowerCase())
);
return (
<div>
<Searchfield
handleChange={(e) => this.setState({ searchfield: e.target.value })}
/>
<CompanyList companies={filteredCompanies} />
</div>
);
}
}
export default App;
Children component:
import React, { Component } from "react";
import { Company } from "../company/company.component";
export class CompanyList extends Component {
constructor(props) {
super(props);
this.state = {
newArray: [],
};
}
componentDidMount = () => {
const filledArray = this.props.companies.map((item) => {
let result;
fetch(`https://xxxxx/incomes/${item.id}`)
.then((response) => response.json())
.then((data) => {
let transactionsToFloat = data.incomes.map((item) =>
parseFloat(item.value)
);
result = transactionsToFloat.reduce((acc, num) => {
return acc + num;
}, 0);
result = Math.round(result * 100) / 100;
});
return {
id: item.id,
name: item.name,
city: item.city,
totalIncome: result,
};
});
this.setState({ newArray: filledArray });
console.log(this.props);
};
render() {
console.log(this.props);
return (
<div>
<table>
<thead>
<tr>
<th> Id </th>
<th> Name </th>
<th> City </th>
<th> Total income </th>
</tr>
</thead>
{this.props.companies.map((item) => (
<Company key={item.id} company={item} />
))}
</table>
</div>
);
}
}
componentWillMount() happens before render(). componentDidMount() happens after.
This is happening because of how React works fundamentally. React is supposed to feel fast, fluent and snappy. the application should never get logged up with http requests or asynchronous code. The answer is to use the lifecycle methods to control the DOM.
What does it mean when a component mounts?
It might be helpful to understand some of the React vocabularies a little better. When a component is mounted it is being inserted into the DOM. This is when a constructor is called. componentWillMount is pretty much synonymous with a constructor and is invoked around the same time. componentDidMount will only be called once after the first render.
componentWillMount --> render --> componentDidMount
Related
From reading previous SO posts and blogs I'm not sure if this is related to props. Either way I'm baffled.
I have a class component, responsible for loading data, which uses a functional component for displaying the data. When the delete button, in the functional component, is pressed it calls props.onDelete which does a fetch and reloads the data. The correct row is deleted from the DB but in the browser it's always the bottom row which is removed. On reloading the page the correct data is displayed.
I've put a breakpoint in the functional component and in the class component render and loadStations methods. On clicking delete button I can see that loadStations is called (which calls setState) and then the functional component is called. However, the render method is never called.
Stations.js (the class component parent)
import React, {Component} from "react";
import EditableTable from "../util/EditableTable";
// column definitions
const columns = [
...
]
export default class Stations extends Component {
constructor() {
super();
this.state = {
stations: []
};
}
componentDidMount () {
this.loadStations();
}
loadStations() {
fetch(`/api/stations`)
.then(response => response.json())
.then(response => {
this.setState({
stations: response.data
})
});
}
saveStation = (e, station) => {
...
}
deleteStation = (e, dataRowIdx) => {
e.preventDefault();
var stationId = this.state.stations[dataRowIdx].stationId;
fetch(`/api/station/${stationId}`, {
method: "DELETE"
})
.then(response => response.json())
.then(data => {
if (data.error) {
this.setState({ error: data.error });
} else {
this.loadStations();
}
}).catch(error => {
this.setState({
error: error.message
});
});
}
render() {
return (
<div>
<h4>Stations</h4>
<EditableTable
columns={columns}
data={this.state.stations}
onDelete={this.deleteStation}
onChanged={this.saveStation}
></EditableTable>
</div>
);
}
}
EditableTable.js (the functional component)
import React, { useState } from "react";
import EditableLabel from "./EditableLabel";
export default function Table(props) {
var emptyDataRow = {};
props.columns.forEach( (column) => {
emptyDataRow[column.property] = ""
});
const [newRowState, setNewRowState] = useState(emptyDataRow);
function cellChanged(e, value, dataRowIdx, columnProperty) {
var dataRow = props.data[dataRowIdx];
dataRow[columnProperty] = value;
props.onChanged(e, dataRow);
}
return <table>
<thead>
<tr>
{props.columns.map( (column, idx) =>
<th key={idx} value>{column.label}</th>
)}
<th></th>
</tr>
</thead>
<tbody>
{props.data.map( (dataRow, dataRowIndex) =>
<tr key={dataRowIndex}>
{props.columns.map( (column, columnIndex) =>
<td key={columnIndex}>
<EditableLabel
value={dataRow[column.property]}
column={column}
onChanged={(e, newValue) => { cellChanged(e, newValue, dataRowIndex, column.property); }}
></EditableLabel>
</td>
)}
<td><button onClick={(e) => { props.onDelete(e, dataRowIndex); }}>delete</button></td>
</tr>
)}
</tbody>
</table>
}
This is most likely because you are using the index given by the map method as the key.
This just uses the item's location in the array as the key, which will not be a sufficient identifier if something in the middle of the array has been removed.
You should give a key that is identifiably unique for that item in the array, eg an unchanging ID or name.
I have one component as below. I am calling on api on its componentDidMount() event. I am not getting why am I not getting its prop value first time when component renders. Also I am not sure why component is rendering 2 times. I have below code.
import React, { Component } from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import AgmtTable from "./AgmtTable";
import * as AgmtAction from "../redux/actions/AgmtAction";
class AgmtContainer extends Component {
constructor(props) {
super(props);
this.state = {};
}
fetch Agmt details.
componentDidMount() {
this.props.dispatch(
AgmtAction.getAgmtsForCustomer(
this.props.match.params.custID,
this.props.match.params.source,
this.props.token
)
);
console.log("componentDidMount", this.props.Agmts);
}
getHeaader = () => {
var tableHeadings = [
"Agmt ID",
"Start Date",
"End Date",
];
return tableHeadings.map((key) => {
return <th key={key}> {key.toUpperCase()}</th>;
});
};
getRowsData = () => {
console.log("in row data", this.props.Agmts);//here I cant see a data though its present in mapStateToProps() function. I am getting error as this.props.agreements.map is not a function.
if (this.props.Agmts) {
return this.props.Agmts.map((value) => {
const {
Agmt_ID,
Agmt_START_DATE,
End_DATE,
} = value;
return (
<tr key={Agmt_ID} className="clickable-row active">
<td> {Agmt_ID} </td>
<td> {Agmt_START_DATE} </td>
<td> {End_DATE} </td>
</tr>
);
});
}
};
render() {
return (
<React.Fragment>
<div>
<table
id="display-table"
className="table table-bordered table-hover table-responsive table-condensed table-striped table-sm"
>
<tbody>
<tr>{this.getHeaader()}</tr>
{this.getRowsData()}
</tbody>
</table>
</div>
</React.Fragment>
);
}
}
function mapStateToProps(state) {
return {
Agmts: state.AgmtsDetails.AgmtsData,//here I have a data
token: state.login.userDetails.token,
};
}
export default connect(mapStateToProps)(AgmtContainer);
Also how can I use the mapStateToProps values to set in state object. When I am running above code I am getting error as this.props.agmts.map is not a function
The dispatch is asynchronous, so you either need to watch for result to be updated in your Redux store with componentDidUpdate or directly return the result from the reducer.
When you get the result, you can manipulate it and store it in local state to reference in your render. Note that unless you need to reference the result in another component somewhere, then you don't need to store it in Redux, you can handle it all in within the component.
Subscribing to the store with componentDidUpdate:
componentDidMount() {
this.props.dispatch(
AgmtAction.getAgmtsForCustomer(
this.props.match.params.custID,
this.props.match.params.source,
this.props.token
)
);
}
componentDidUpdate(prevProps) {
if (JSON.stringify(prevProps.Agmts) !== JSON.stringify(this.props.Agmts)) {
// this is the result of the dispatch
console.log(this.props.Agmts);
}
}
Returning the result directly back:
// in your AgmtAction.getAgmtsForCustomer action
export const getAgmtsForCustomer = () => (dispatch, getState) => {
return axios
.get(..........
.then((res) => {
dispatch(..........
return res.data;
})
.catch((err) => {
...
});
};
// in your `AgmtContainer` component
...
componentDidMount() {
this.props.dispatch(
AgmtAction.getAgmtsForCustomer(
this.props.match.params.custID,
this.props.match.params.source,
this.props.token
)
).then((res) => {
// this is the result of the dispatch
console.log(res);
});
}
In getRowsData function where you are getting error "map is not a function" is due to the data you are getting in this.props.Agmts must be an object type. (Object encloses in curly brackets {}).
You can apply map function only on array not on an object. (Array encloses in square brackets [])
I have an array set up in a table so that when I click on an icon, a different icon is re-rendered in that specific row. Now I have localstorage setup so that when I refresh the page, the state is maintained. The problem I am having though is if I click 4 icons, 4 different icons are obviously re-rendered but when I refresh the page, the last one I click is reverted to its original state so now I only have 3 of the re-rendered icons. How do I fix this?
import React from 'react';
import StarBorder from '#material-ui/icons/StarBorder';
import Star from '#material-ui/icons/Star';
import axios from 'axios';
class Test extends React.Component {
constructor(props) {
super(props);
var newData = [];
if (localStorage.getItem('new')) {
newData = JSON.parse(localStorage.getItem('new'));
}
this.state = {
data: newData,
}
}
componentDidMount() {
if (!localStorage.getItem('new')) {
axios.get('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=100&page=1&sparkline=true')
.then(res => {
const data = res.data;
this.setState({ data: data.map(x => ({...x, starIcon: true})) })
})
}
}
handleClick = (i) => {
this.setState(prevState => ({
data: prevState.data.map((x, key) => (key === i ? {...x, starIcon: !x. starIcon} : x))
}));
localStorage.setItem('new', JSON.stringify(this.state.data));
}
render() {
return (
<div>
<table border="1">
<thead>
<tr>
<td>Icon</td>
<td>Name</td>
<td>Price</td>
</tr>
</thead>
<tbody>
{this.state.data.map((n, i) => {
return (
<tr>
<td> <span onClick={() => this.handleClick(i)}> {n.starIcon ? <StarBorder/> : <Star /> } </span> </td>
<td>{n.name}</td>
<td>{n.current_price}</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
}
export default Test;
setState is async, so you when you set items to local storage you may still capture previous state. To fix it, use setState's callback function to make sure the state is updated when you set the items:
handleClick = (i) => {
this.setState(prevState => ({
data: prevState.data.map((x, key) => (key === i ? {
...x,
starIcon: !x.starIcon
} : x))
}), () => {
localStorage.setItem('new', JSON.stringify(this.state.data));
});
}
You will have to change handleClick function, Because before setState set its state, local storage setitem method getting called so.
So you have to use callback function of setState method
Why is setState in reactjs Async instead of Sync?
handleClick = (i) => {
this.setState(prevState => ({
data: prevState.data.map((x, key) => (key === i ? {
...x,
starIcon: !x.starIcon
} : x))
}), () => {
localStorage.setItem('new', JSON.stringify(this.state.data));
});
}
I'm working with react in Laravel, and i'm trying to built a simple FriendsList component to display data from API.
The problem is that the Parent (Profile) component is finish loading before it's get the data, so the FriendsList component return an error, because the props are empty for the first time. It's important to say that regardless of the API response - the parent (Profile) component works well, it's loaded for the first time empty - and then the data is adding.
The Api call
export const getProfile = () => {
return axios
.get('api/profile', {
headers: { Authorization: `Bearer ${localStorage.usertoken}` }
})
.then(response => {
// console.log(response.data)
return response.data
})
.catch(err => {
console.log(err)
})
}
The Parent Component
import React, { Component } from 'react'
import { getProfile } from './UserFunctions'
import FriendsList from './FriendsList';
class Profile extends Component {
constructor() {
super()
this.state = {
name: '',
hobbies: '',
user_bday: '',
members: [],
error: '',
}
}
componentDidMount() {
getProfile().then(res => {
// console.log(JSON.parse(res))
this.setState({
name: res.user.name,
hobbies: res.user.hobbies,
user_bday: res.user.user_birthday,
related_friends: res.user.related_friends,
members: res.user.members,
})
})
}
render() {
return (
<div className="container">
<div className="jumbotron mt-5">
<div className="col-sm-4 mx-auto">
<h1 className="text-center">PROFILE</h1>
</div>
<table className="table col-md-4 mx-auto">
<tbody>
<tr>
<td>Name</td>
<td>{this.state.name}</td>
<td>{this.state.hobbies}</td>
<td>{this.state.user_bday}</td>
</tr>
</tbody>
</table>
<FriendsList members={this.state.members}>
</FriendsList>
</div>
</div>
)
}
}
export default Profile
import React from 'react';
class FriendsList extends React.Component {
render() {
console.log(this.props)
const { members } = this.props;
const listmembers = members.map((item, index) => (
<li key={item + index}>{item.name}</li>
));
return (
<div>
{listmembers}
</div>
);
}
}
export default FriendsList
There are a couple of ways to go about this.
First approach:
class Profile extends Component {
render() {
// check if your `state` has all the necessary values
// before rendering your JSX
const { name, hobbies, user_bday, members } = this.state
const shouldRender = name !== '' &&
hobbies !== '' &&
user_bday !== '' &&
Array.isArray(members) && members.length > 0
if (!shouldRender) {
return null;
}
return (...)
}
}
This way, you're only rendering JSX when your state has everything that you need.
Second approach:
class Profile extends Component {
constructor() {
// ...
this.setState = {
members: []
}
}
}
Set your members to an empty array, rather than an empty string, so that way when you're passing it as prop to FriendList component, calling this.props.friends.map is actually correct, but it won't render anything since the array is initially empty.
Also, it looks like you are never updating your members after your API call finishes:
componentDidMount() {
getProfile().then(res => {
this.setState({
name: res.user.name,
hobbies: res.user.hobbies,
user_bday: res.user.user_birthday,
related_friends: res.user.related_friends,
})
})
}
So your members actually stays as an empty string. Make sure your updating your state with the right type, which in this case should be an array.
if I understood your question I guess you need to take a look at https://www.npmjs.com/package/prop-types, I think your problem is with the default props value and this library could help you achieve the wanted behavior.
If I understood the question correctly you're talking about null props... Also, Kellen in the comments is correct... You should set members to an empty array instead and I do not see you updating the state for members in your setState...
Try:
render() {
const friendsList =
this.props.firends &&
this.props.friends.map(function(item, index) {
<li key={index}>{item.name}</li>;
});
return (
<div>
<ul>{friendsList}</ul>
</div>
);
}
Another approach here would be to use loading state, during which you'll show a loading indicator, etc.
class Profile extends Component {
constructor() {
super();
this.state = {
name: "",
hobbies: "",
user_bday: "",
members: "",
error: "",
isMembers: false,
loading: true // Add loading state
};
}
componentDidMount() {
getProfile().then(res => {
// console.log(JSON.parse(res))
this.setState({
name: res.user.name,
hobbies: res.user.hobbies,
user_bday: res.user.user_birthday,
related_friends: res.user.related_friends,
loading: false // Remove the loading state when data is fetched
});
});
}
render() {
return this.state.loading ? (
<p> Loading... </p>
) : (
<div className="container">
<div className="jumbotron mt-5">
<div className="col-sm-4 mx-auto">
<h1 className="text-center">PROFILE</h1>
</div>
<table className="table col-md-4 mx-auto">
<tbody>
<tr>
<td>Name</td>
<td>{this.state.name}</td>
<td>{this.state.hobbies}</td>
<td>{this.state.user_bday}</td>
</tr>
</tbody>
</table>
{/*<FriendsList friends={this.state.members}></FriendsList>*/}
</div>
</div>
);
}
}
I have a set of Reacts components that load asynchronously to deal with a large dataset. I have an initial loader working through react-spinners BeatLoader, which works fine. My problem arises when I try to replicate this effect with a Loading inventory... message when trying to pass in the productsLoading state in its parent component as I did for the warehouse loading state at the index.jsx level.
I'm only trying to get a callback working in the child component for this.
index.jsx:
import React from 'react'
import { withRouter } from 'react-router-dom'
import WarehouseDetails from './warehouse-details'
import { BeatLoader } from 'react-spinners';
;
class WarehouseReport extends React.Component {
state = {
warehouse: {
name: '',
sales: '',
cost: ''
},
categories: [],
products: [],
warehouseLoading: true, <<< top level loader
categoriesLoading: true,
productsLoading: true, <<< component level
}
componentDidMount() {
this.loadWarehouseInfo()
}
loadWarehouseInfo = () => {
return fetch(`//${window.location.host}/${this.props.match.params.account_id}/warehouse_info/${this.props.match.params.warehouse_id}`)
.then(response => response.json())
.then(json => {
this.setState({
warehouse: {
name: json.warehouse_name,
sales: json.warehouse_sale_total,
cost: json.warehouse_cost_total
},
warehouseLoading: false
}, this.loadCategoryInfo)
})
}
loadCategoryInfo = () => {
return fetch(`//${window.location.host}/${this.props.match.params.account_id}/warehouse_info/${this.props.match.params.warehouse_id}/categories`)
.then(response => response.json())
.then(json => {
this.setState({
categories: json,
categoriesLoading: false
}, this.loadProductInfo)
})
}
loadProductInfo = () => {
return fetch(`//${window.location.host}/${this.props.match.params.account_id}/warehouse_info/${this.props.match.params.warehouse_id}/category_products/${this.state.categories.map(category => category.id).join(',')}`)
.then(response => response.json())
.then(json => {
this.setState({
products: json,
productsLoading: false, <<<< setup states
loading: false
})
})
}
render() {
const { warehouse, categories, products, warehouseLoading } = this.state
return <div>
{ warehouseLoading ? <BeatLoader
color={'#4db3bf'}
/> : <WarehouseDetails warehouse={warehouse} categories={categories} products={products} /> }
</div>
}
}
export default withRouter(WarehouseReport)
When I drill down to the component I'm trying to set up the loader for, I have this:
category-details.jsx:
import React from 'react'
import ProductDetails from './product-details'
import NumberFormat from 'react-number-format';
export default function CategoryDetails({ name, count, products, productsLoading }) {
<<< state is passed down; passing state outside the curly brackets only replaces the following component with the loading statement/item)
return <div>
<div><h3>{name}</h3></div>
<div>{count} products found</div>
<h4>Products</h4>
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Rental $</th>
<th>Sale $</th>
</tr>
</thead>
<tbody>
{ this.state.productsLoading ? 'Loading inventory...' : products.map(product => <ProductDetails
{...product}
/>) <<<< component-level loader here
}
</tbody>
</table>
</div>
}
What's the correct way of getting this loader in place? Thanks for your time, I'm still getting a handle on states in React.
I think something you're looking for is:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all