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));
});
}
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'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
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 am using WebSockets to update upvotes on comments in React. I am receiving comment updates in logs of different client instances. However, React does not render the updates to upvotes.
Code I am trying:
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
class Comment extends Component {
constructor(props){
super(props);
this.upvotes = React.createRef();
this.downvotes = React.createRef();
this.handleUpvote = this.handleUpvote.bind(this);
this.handleDownvote = this.handleDownvote.bind(this);
}
handleUpvote(){
console.log(this.props);
const json = { type: 'upvote' };
json.data = this.props;
json.data.comment.upvotes++;
console.log(json);
this.props.socket.send(JSON.stringify(json));
}
handleDownvote(){
this.downvotes.current.innerHTML++;
console.log(this.downvotes.current.innerHTML);
}
render() {
return (
<tr>
<td>{this.props.comment.user.firstName} {this.props.comment.user.lastName}</td>
<td>{this.props.comment.content }</td>
<td> <span ref={this.upvotes}>{this.props.comment.upvotes}</span> <button onClick={this.handleUpvote}>Upvote</button> </td>
<td> <span ref={this.downvotes}>{this.props.comment.downvotes}</span> <button onClick={this.handleDownvote}>Downvote</button> </td>
</tr>
)
}
}
export default class ListComments extends Component {
constructor(props){
super(props);
this.state = { comments: [] }
}
componentDidMount(){
axios.get('http://localhost:5000/api/comments/')
.then(resp => this.setState({ comments : resp.data }))
.catch(err => console.log(err));
}
componentWillReceiveProps(nextProps){
const data = JSON.parse(nextProps.comment);
console.log(data.data);
if(data.type === "upvote"){
// const a = this.state.comments;
// a.forEach(comment => {
// if(comment._id == data.data.comment._id){
// comment = data.data.comment
// }
// });
// this.setState({ comments : a })
this.setState(prevState => {
// Get previous state
const { comments } = prevState;
// Add new item to array
comments.forEach(comm => {
if(comm._id == data.data.comment._id){
comm = data.data.comment
}
});
// Return new state
return { comments };
});
}
else if(data.type === "comment"){
this.setState({ comments : [data.data, ...this.state.comments] })
}
}
commentList() {
return this.state.comments.map(currentcomment => {
return <Comment comment={currentcomment} socket={this.props.actions} key={currentcomment._id}/>;
})
}
render() {
return (
<div>
<h3>Comments</h3>
<table className="table">
<thead className="thead-light">
<tr>
<th>Username</th>
<th>Content</th>
<th>Upvotes</th>
<th>Downvotes</th>
</tr>
</thead>
<tbody>
{ this.commentList() }
</tbody>
</table>
</div>
);
}
}
Outputs I am getting -
Client one with 3 upvotes to question 1
Client 2 with updates to upvotes received in console, not rendred in actual comment
I am building an application that requires a table of items to be sorted and change the orderNumber of them depending on their sorting. I installed and utilized a library called react-dnd to handle the functionality of sorting/ordering, and its working great so far. The issue im having is the update. When a user moves one of the items, I need to send a PUT request to the api and update its orderNumber. It was working last night great, here is my code.
The ListItem (Item that is being sorted and updated):
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import {DragSource, DropTarget} from 'react-dnd';
import sdk from '../../js/sdk';
import ItemTypes from './ItemTypes';
const itemSource = {
beginDrag(props) {
return {id: props.id};
}
};
const itemTarget = {
hover(props, monitor) {
const draggedId = monitor.getItem().id;
if (draggedId !== props.id) {
props.swapItems(draggedId, props.id);
}
}
};
const DragSourceDecorator = DragSource(ItemTypes.ITEM, itemSource, (connect, monitor) => {
return {
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
};
});
const DropTargetDecorator = DropTarget(ItemTypes.ITEM, itemTarget, (connect) => {
return {connectDropTarget: connect.dropTarget()};
});
class SwagBagItem extends React.Component {
constructor(props) {
super(props);
this._handleDelete = this._handleDelete.bind(this);
}
componentWillReceiveProps(nextProps) {
const swagbagItemCpy = Object.assign({}, nextProps.swagbagItem);
delete swagbagItemCpy.id;
if (nextProps) {
sdk.put(`swagbags/${nextProps.swagbag.id}/items/${nextProps.swagbagItem.id}`, swagbagItemCpy)
.done((result) => {
console.log(result);
}).fail((error) => {
console.log(error);
})
;
}
}
_handleDelete(event) {
event.preventDefault();
event.stopPropagation();
if (confirm('Are you sure you want to delete this Swagbag Item?')) {
sdk.delete(`swagbags/${this.props.swagbag.id}/items/${this.props.swagbagItem.id}`)
.done(() => {
console.log('Swagbag Item remove!');
}).then(() => {
this.props.loadSwagBags();
});
}
}
render() {
const {swagbagItem} = this.props;
return this.props.connectDragSource(this.props.connectDropTarget(
<tr className="swagbag-item">
<td>{swagbagItem.id}</td>
<td><Link to={`${this.props.swagbag.id}/items/${swagbagItem.id}`}>{swagbagItem.name}</Link></td>
<td>{swagbagItem.uri}</td>
<td>
<div className="btn-group btn-group-xs pull-right" role="group">
<Link to={`${this.props.swagbag.id}/items/${swagbagItem.id}/edit`} className="btn btn-info">Edit</Link>
<Link to={`${this.props.swagbag.id}/items/${swagbagItem.id}`} className="btn btn-info">View</Link>
<button className="btn btn-danger btn-xs" onClick={this._handleDelete}>Remove</button>
</div>
</td>
</tr>
));
}
}
SwagBagItem.propTypes = {
loadSwagBags: PropTypes.func,
params: PropTypes.object,
swagbag: PropTypes.object,
swagbagItem: PropTypes.object,
};
export default DropTargetDecorator(DragSourceDecorator(SwagBagItem));
The container or list that holds these items:
import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import {DragDropContext} from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import sdk from '../../js/sdk';
import Nav from '../Nav';
import SwagBagItem from '../SwagBagItem';
class SwagBagItemsList extends React.Component {
constructor(props) {
super(props);
this.state = {
swagbag: null,
swagbagItems: [],
};
this._loadSwagBags = this._loadSwagBags.bind(this);
this._compareItems = this._compareItems.bind(this);
this._swapItems = this._swapItems.bind(this);
}
componentWillMount() {
this._loadSwagBags();
}
_compareItems(item1, item2) {
return item1.orderNumber - item2.orderNumber;
}
_swapItems(itemNo1, itemNo2) {
const items = this.state.swagbagItems;
let item1 = items.filter(item => item.id === itemNo1)[0];
let item2 = items.filter(item => item.id === itemNo2)[0];
let item1Order = item1.orderNumber;
item1.orderNumber = item2.orderNumber;
item2.orderNumber = item1Order;
items.sort(this._compareItems);
this.setState({swagbagItems: items});
}
_loadSwagBags() {
sdk.getJSON(`swagbags/${this.props.params.id}`)
.done((result) => {
this.setState({swagbag: result});
})
.then(() => {
sdk.getJSON(`swagbags/${this.props.params.id}/items?fields=id,name,summary,uri,itemImageFile,orderNumber`).done((results) => {
this.setState({swagbagItems: results});
});
});
}
render() {
let swagbagItems = null;
if (this.state.swagbagItems) {
swagbagItems = this.state.swagbagItems.map((item) => {
return <SwagBagItem
loadSwagBags={this._loadSwagBags}
swagbag={this.state.swagbag}
swagbagItem={item}
key={item.id}
id={item.id}
swapItems={this._swapItems}
/>;
});
}
if (!this.state.swagbag) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Swagbag Items</h1>
<Nav swagbag={this.state.swagbag} />
<table className="table">
<thead>
<tr>
<th>id</th>
<th>name</th>
<th>uri</th>
<th></th>
</tr>
</thead>
<tbody>
{swagbagItems}
</tbody>
</table>
<Link to={`swagbags/createItem/swagbagid/${this.state.swagbag.id}`} className="btn btn-success">Add Item</Link>
</div>
);
}
}
SwagBagItemsList.propTypes = {
params: PropTypes.object,
};
export default DragDropContext(HTML5Backend)(SwagBagItemsList);
It is making the PUT request, but its making hundreds of them in a row from just moving one object. I cant for the life of me figure out why. This puts a severe lag on the application and makes it unresponsive. Am I going about this the right way, and if so, what is the solution to this?
EDIT #1: Woke up today and the application is working fine. Unfortunately this is going in production, so before that I have to recreate the bug of 800+ PUT requests and figure it out. Might put a bounty on this.
If you want to get it so that it sends the update once it's finished dragging, there's an endDrag function you can add to your DragSource (http://gaearon.github.io/react-dnd/docs-drag-source.html) that will only be fired once and will only be fired upon finishing the drag. So if you remove your api call from componentWillReceiveProps and move it to the source like this:
const itemSource = {
beginDrag(props) {
return {
id: props.id,
swagbagId: props.swagbag.id,
swagbagItem: props.swagbagItem,
};
},
endDrag(props, monitor) {
const item = monitor.getItem();
sdk.put(`swagbags/${item.swagbagId}/items/${item.swagbagItem.id}`, item.swagbagItem)
.done((result) => {
console.log(result);
}).fail((error) => {
console.log(error);
})
;
},
};
It should only make the call one time (I can't perfectly predict that without knowing what's in swagbag and swagbagItem but I think it should). Note that I'm using the getItem() function from the DragSource monitor (http://gaearon.github.io/react-dnd/docs-drag-source-monitor.html) to retrieve what was passed in upon beginDrag.