Grid view is not being shown with space, rows and column wise and When I am clicking delete menu item, it is passing the last array value (last card's value) to the function, not the clicked card's value. Something is wrong in Grid view.
Following is the data used by the cards. Import statements are there.
Array:
0: {id: "5", title: "Java", price: "78$"}
1: {id: "2", title: "C++", price: "79$"}
2: {id: "4", title: "C", price: "127$"}
3: {id: "1", title: ".Net", price: "65$"}
4: {id: "3", title: "React Js", price: "67$"}
This is the code of my component:
const styles = theme => ({
root: {
flexGrow: 1,
},
paper: {
padding: theme.spacing.unit * 2,
textAlign: 'center',
color: theme.palette.text.secondary,
},
card: {
maxWidth: 400,
},
media: {
height: 0,
paddingTop: '56.25%', // 16:9
},
actions: {
display: 'flex',
},
});
const ITEM_HEIGHT = 40;
class Products extends Component {
constructor() {
super();
this.state = {
products: [],
searchString: ''
};
this.getProducts()
}
state = {
anchorEl: null,
};
handleClick = event => {
this.setState({ anchorEl: event.currentTarget });
};
handleClose = () => {
this.setState({ anchorEl: null });
};
delete = id => {
alert(id)
axios.post('http://localhost:9022/products/delete/' + id)
.then(res => {
let updatedProducts = [...this.state.products].filter(i => i.id !== id);
this.setState({ products: updatedProducts });
});
}
getProducts() {
axios.get('http://localhost:9022/products/getAll')
.then(res => {
this.setState({ products: res.data });
console.log(this.state.products);
});
}
onSearchInputChange = (event) => {
if (event.target.value) {
this.setState({ searchString: event.target.value })
} else {
this.setState({ searchString: '' })
}
this.getProducts()
}
render() {
const { anchorEl } = this.state;
const open = Boolean(anchorEl);
const { classes } = this.props;
return (
<div>
<TextField style={{ padding: 24 }}
id="searchInput"
placeholder="Search for products"
margin="normal"
onChange={this.onSearchInputChange} />
<Grid container spacing={12}>
<Grid item xs={4} xm={4}>
<div className="row">
{this.state.products.map(currentProduct => (
<div key={currentProduct.id}>
<Card>
<CardHeader
action={
<IconButton aria-label="More"
aria-owns={open ? 'long-menu' : null}
aria-haspopup="true"
onClick={this.handleClick}>
<MoreVertIcon />
<Menu
id="long-menu"
anchorEl={anchorEl}
open={open}
onClose={this.handleClose}
PaperProps={{
style: {
maxHeight: ITEM_HEIGHT * 4.5,
width: 100,
},
}}
>
<MenuItem component={Link} to={'/products/' + currentProduct.id}>Edit
</MenuItem>
<MenuItem onClick={() => this.delete(currentProduct.id)}>Delete
</MenuItem>
</Menu>
</IconButton>
}
title={currentProduct.title}
/>
<CardContent>
<Typography component="p">
{currentProduct.id}
</Typography>
</CardContent>
</Card>
</div>
))}
</div>
</Grid>
</Grid>
</div>
)
}
}
export default withStyles(styles)(Products);
I see what the problem is. It's a problem with your code logic.
What you're trying to do in the action section of the CardHeader is rendering a Menu that has two static items in it
<MenuItem component={Link} to={'/products/' + currentProduct.id}>Edit</MenuItem> <MenuItem onClick={() => this.delete(currentProduct.id)}>Delete</MenuItem>
The thing is the Menu has to have a unique id, but each time you render it you give the same => simple-menu instead you could do something like simple-menu-${currentProduct.id}. And the best to do is that you render a separate component from the CardHeader instead of actions.
This gives you more control over you component and each element you want to render.
See and edit it here:
I personally don't like to put a Menu inside of that card, instead I'd put icons to the top left/right of the card.
Uncomment the action property in the CardHeader and comment out the component one to see what I mean!
I hope that's clear, let me know if it isn't!
There are some things that you can fix which should solve all your problems, or at least guide you through the right direction.
I will guide you through, but it would be ideal if I you read the docs first.
You are calling this.getProducts() inside your constructor, and that function uses setState
You are initialising and setting anchorEl in the state outside of the constructor
You are calling super without passing on props, which might lead to bugs
You are not binding functions that use this (handleClick, handleClose, getProducts, etc), which could lead to undefined state of this.
You are calling functions that get values from the state right after calling setState, which might not get the correct values because of how setState works in React.
You should avoid all.
Constructor, bindings, first time fetch
Constructor
Constructor from the official docs:
You should not call setState() in the constructor(). Instead, if your
component needs to use local state, assign the initial state to
this.state directly in the constructor:
The constructor for a React component is called before it is mounted.
When implementing the constructor for a React.Component subclass, you
should call super(props) before any other statement. Otherwise,
this.props will be undefined in the constructor, which can lead to
bugs.
Typically, in React constructors are only used for two purposes:
Initializing local state by assigning an object to this.state.
Binding event handler methods to an instance.
Your code:
constructor() {
super();
this.state = {
products: [],
searchString: ''
};
this.getProducts()
}
state = {
anchorEl: null,
};
Change it to:
constructor(props) {
super(props);
this.state = {
products: [],
searchString: '',
anchorEl: null,
};
this.onSearchInputChange = this.onSearchInputChange .bind(this);
this.getProducts = this.getProducts.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleClose = this.handleClose.bind(this);
}
First time fetch
To call this.getProducts() when the app starts, don't use the constructor, use componentDidMount instead.
componentDidMount from the official docs:
componentDidMount() is invoked immediately after a component is
mounted (inserted into the tree). Initialization that requires DOM
nodes should go here. If you need to load data from a remote endpoint,
this is a good place to instantiate the network request.
Create this function inside the component:
componentDidMount(){
this.getProducts();
}
Bindings
Binding from the official docs:
There are several ways to make sure functions have access to component
attributes like this.props and this.state, depending on which syntax
and build steps you are using
- Bind in Constructor (ES2015)
- Class Properties (Stage 3 Proposal)
- Bind in Render
You can use any of these, but I suggest you use the first one.
Your functions:
handleClick = event => {
this.setState({ anchorEl: event.currentTarget });
};
handleClose = () => {
this.setState({ anchorEl: null });
};
Change them to:
handleClick(event) {
this.setState({ anchorEl: event.currentTarget });
};
handleClose() {
this.setState({ anchorEl: null });
};
setState
Correct use of setState
setState from the official docs
setState(updater[, callback])
setState() enqueues changes to the component state and tells React
that this component and its children need to be re-rendered with the
updated state. This is the primary method you use to update the user
interface in response to event handlers and server responses.
setState() does not always immediately update the component. It may
batch or defer the update until later. This makes reading this.state
right after calling setState() a potential pitfall. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied. If you need to set the state based on the previous
state, read about the updater argument below.
this.setState((state, props) => {
return {counter: state.counter + props.step};
});
So you should not do this:
onSearchInputChange = (event) => {
if (event.target.value) {
this.setState({ searchString: event.target.value })
} else {
this.setState({ searchString: '' })
}
this.getProducts()
}
Because you cannot guarantee that when this.getProducts() is called, the previous setState functions have finished. This means that it might work most of the times, but there would be some cases when React hasn't finished updating the state and you are already calling this.getProducts().
Instead, you should call this.getProducts() once setState has finished, and to guarantee that just use the callback like this (and I am also changing the function's declaration because we already bound it in the constructor with the previous change):
onSearchInputChange(event) {
let newSearchString = '';
if (event.target.value) {
newSearchString = event.target.value;
}
// call getProducts once React has finished updating the state using the callback (second argument)
this.setState({ searchString: newSearchString }, () => {
this.getProducts();
});
}
Your getProducts is OK (now that we bound it in the constructor), but you are calling console.log when it should not be called:
getProducts() {
axios.get('http://localhost:9022/products/getAll')
.then(res => {
this.setState({ products: res.data });
console.log(this.state.products);
});
}
Based on the previous explanation of setState, call it like this:
getProducts() {
axios.get('http://localhost:9022/products/getAll')
.then(res => {
this.setState({ products: res.data }, () => {
console.log(this.state.products);
});
});
}
Your delete function
Assuming that your data is in fact an array, like this:
products: [
{id: "5", title: "Java", price: "78$"}
{id: "2", title: "C++", price: "79$"}
{id: "4", title: "C", price: "127$"}
{id: "1", title: ".Net", price: "65$"}
{id: "3", title: "React Js", price: "67$"}
]
the code that you have should work with the previous changes in the component. But, there is something you can improve as well.
This is your code:
delete = id => {
alert(id)
axios.post('http://localhost:9022/products/delete/' + id)
.then(res => {
let updatedProducts = [...this.state.products].filter(i => i.id !== id);
this.setState({ products: updatedProducts });
});
}
I will refer back to the docs to the setState documentation where the updater function is explained:
setState(updater[, callback])
The first argument is an updater function with the signature:
(state, props) => stateChange
state is a reference to the component state at the time the change is
being applied. It should not be directly mutated. Instead, changes
should be represented by building a new object based on the input from
state and props. For instance, suppose we wanted to increment a value
in state by props.step:
this.setState((state, props) => {
return {counter: state.counter + props.step};
});
Both state and props received by the updater function
are guaranteed to be up-to-date. The output of the updater is
shallowly merged with state.
It's important to understand when to use this updater function, and the state param in that function.
The easiest case is the one they mention:
this.setState((state, props) => {
return {counter: state.counter + props.step};
});
This could be done like this:
this.setState({counter: this.state.counter + this.props.step});
But since you cannot guarantee that setState has been successful and that it has finished updating the values, you should do it using the updater function.
Now, back to your delete function.
Change this:
delete = id => {
alert(id)
axios.post('http://localhost:9022/products/delete/' + id)
.then(res => {
let updatedProducts = [...this.state.products].filter(i => i.id !== id);
this.setState({ products: updatedProducts });
});
}
to this (notice that I changed the parameter name state to prevState in the updater function so that it makes more sense and it's easier to understand):
delete = id => {
alert(id);
axios.post('http://localhost:9022/products/delete/' + id)
.then(res => {
// To guarantee you get the correct values, get them from the state in the updater function in setState
this.setState((prevState, prevProps) => {
// This happens inside the setState function
let updatedProducts = [...prevState.products].filter(i => i.id !== id);
// The updater function must return the values that will be modified in the state
return ({
products: updatedProducts
});
});
});
}
It's important to notice that filtering before setState like this:
let updatedProducts = [...this.state.products].filter(i => i.id !== id);
this.setState({ products: updatedProducts });
Will work most of the times but it is not recommended, use the updater function instead when handling this situations to ensure everything works every time.
Related
I'm using Antd Modal to display order information by passing props to TableDetail Component. The thing is when I try to remove the items in the order I use setState and the modal won't update its data.
https://codesandbox.io/s/antd-reproduction-template-forked-vu3ez
A simple version of reproduction is provided above.
enter image description here
After I click on the remove button the items remain unchanged but when I close and open the modal it will update.
enter image description here
What should I do to make it change? I tried forceUpdate() but it won't work.
Here is the TableDetail component.
export default class TableDetail extends Component {
constructor(props) {
super(props);
this.state = {
tableOrder: props.tableOrder,
column: []
};
}
tableInitializer = () => {
const column = [
{
title: "name",
render: (item) => {
return <span>{item.name}</span>;
}
},
{
title: "price",
render: (item) => {
return item.price;
}
},
{
title: "amount",
render: (item) => {
return <span>{item.amount}</span>;
}
},
{
title: "removeButton",
render: (item) => {
return (
<Button
type="link"
style={{ color: "red" }}
onClick={() => this.props.removeItem(item)}
>
Remove
</Button>
);
}
}
];
this.setState({
column
});
};
UNSAFE_componentWillMount() {
this.tableInitializer();
}
render() {
return (
<Modal
visible={true}
title={
<span style={{ fontSize: "1.5rem", fontWeight: "bold" }}>
Table: {this.state.tableName}
</span>
}
cancelText="Cancel"
okText="OK"
onCancel={this.props.hideModal}
>
<Table
columns={this.state.column}
dataSource={this.state.tableOrder}
rowKey="name"
/>
</Modal>
);
}
}
TableDetail.propTypes = {
tableOrder: PropTypes.array.isRequired,
tableName: PropTypes.string.isRequired,
hideModal: PropTypes.func.isRequired,
removeItem: PropTypes.func.isRequired
};
You can use the filter method in your removeItem function
removeItem = (item) => {
const filtered = this.state.tableOrder.filter(
(order) => item.name !== order.name
);
console.log(item, filtered);
this.setState({
tableOrder: filtered
});
};
and one more thing you should avoid assigning your props to state like
this.state = {
tableOrder: props.tableOrder,
};
instead of assigning to state directly use that as props like
<Table
dataSource={this.props.tableOrder}
/>
In most cases, this is an antipattern. Don’t “copy props into state.”
It creates a second source of truth for your data, which usually leads
to bugs. One source of truth is best.
Improved code Live demo
The issue in your code is in removeItem. You are trying to mutate tableOrder. According to react docs:
Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.
So that is why it is kind of asynchronous and you can't see your updated changes. You need to do this:
removeItem = (item) => {
const { tableOrder } = this.state;
const newTable = [...tableOrder]; <- Create new variable and update variable
for (let i = 0; i < newTable.length; i++) {
if (item.name === newTable[i].name) {
console.log("Got it! " + newTable[i].name);
newTable.splice(i, 1);
console.log("new table: ", tableOrder);
break;
}
}
this.setState({
tableOrder: newTable <- update here
});
};
You dont need to actually modify array you can simply do this also:
this.setState(
{
tableOrder: this.state.tableOrder.filter(data => !(data.name===item.name))
}
)
//filter creates new array
Here is demo: https://codesandbox.io/s/antd-reproduction-template-forked-j4bfl?file=/tables.js
I click Item -> I get data from url:https: // app / api / v1 / asset / $ {id}. The data is saved in loadItemId. I am moving loadItemId from the component Items to the component Details, then to the component AnotherItem.
Each time I click Item the props loadItemId changes in the getDerivedStateFromProps method. Problem: I'll click Element D -> I see in console.log 'true', then I'll click Element E --> It display in console.log true andfalse simultaneously, and it should display only false.
Trying to create a ternary operator {this.state.itemX ['completed'] ? this.start () : ''}. If {this.state.itemX ['completed'] call the function this.start ()
Code here: stackblitz
Picture: https://imgur.com/a/OBxMKCd
Items
class Items extends Component {
constructor (props) {
super(props);
this.state = {
itemId: null,
loadItemId: ''
}
}
selectItem = (id) => {
this.setState({
itemId: id
})
this.load(id);
}
load = (id) => {
axios.get
axios({
url: `https://app/api/v1/asset/${id}`,
method: "GET",
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => {
this.setState({
loadItemId: response.data
});
})
.catch(error => {
console.log(error);
})
}
render () {
return (
<div >
<Item
key={item.id}
item={item}
selectItem={this.selectItem}
>
<Details
loadItemId={this.state.loadTime}
/>
</div>
)
}
Item
class Item extends Component {
render () {
return (
<div onClick={() => this.props.selectItem(item.id}>
</div>
)
}
}
Details
class Details extends Component {
constructor() {
super();
}
render () {
return (
<div>
<AnotherItem
loadItemId = {this.props.loadItemId}
/>
</div>
)
}
}
AnotherItem
class AnotherItem extends Component {
constructor() {
super();
this.state = {
itemX: ''
};
}
static getDerivedStateFromProps(nextProps, prevState) {
if(nextProps.loadItemId !== prevState.loadItemId) {
return { itemX: nextProps.loadItemId }
}
render () {
console.log(this.state.itemX ? this.state.itemX['completed'] : '');
{/*if this.state.loadX['completed'] === true, call function this.start()*/ }
return (
<button /*{this.state.loadX['completed'] ? this.start() : ''}*/ onClick={this.start}>
Start
</button>
);
}
}
here:
selectItem = (id) => {
this.setState({
itemId: id
})
this.load(id);
}
you call setState(), then 'Item' and 'Details' and 'AnotherItem' call their render method. so you see log for previous 'loadItemId'.
when 'load' method work done. here:
this.setState({
loadItemId: response.data
});
you setState() again, then 'Item' and 'Details' and 'AnotherItem' call their render method again. in this time you see log for new 'loadItemId'.
solution
setState both state in one place. after load method done, instead of:
this.setState({
loadItemId: response.data
});
write:
this.setState({
itemId: id,
loadItemId: response.data
});
and remove:
this.setState({
itemId: id
})
from 'selectItem' method.
Need some clarification, but think I can still address this at high level. As suggested in comment above, with the information presented, it does not seem that your component AnotherItem actually needs to maintain state to determine the correct time at which to invoke start() method (although it may need to be stateful for other reasons, as noted below).
It appears the functionality you are trying to achieve (invoke start method at particular time) can be completed solely with a comparison of old/new props by the componentDidUpdate lifecycle method. As provided by the React docs, getDerivedStateFromProps is actually reserved for a few 'rare' cases, none of which I believe are present here. Rather, it seems that you want to call a certain method, perhaps perform some calculation, when new props are received and meet a certain condition (e.g., not equal to old props). That can be achieved by hooking into componentDidUpdate.
class AnotherItem extends Component {
constructor(props) {
super(props);
this.state = {}
}
start = () => { do something, perform a calculation }
// Invoked when new props are passed
componentDidUpdate(prevProps) {
// Test condition to determine whether to call start() method based on new props,
// (can add other conditionals limit number of calls to start, e.g.,
// compare other properties of loadItemId from prevProps and this.props) .
if (this.props.loadItemId && this.props.loadItemId.completed === true) {
//Possibly store result from start() in state if needed
const result = this.start();
}
}
}
render () {
// Render UI, maybe based on updated state/result of start method if
// needed
);
}
}
You are encountering this behaviour because you are changing state of Items component on each click with
this.setState({
itemId: id
})
When changing its state, Items component rerenders causing AnotherItem to rerender (because that is child component) with it's previous state which has completed as true (since you've clicked element D before). Then async request completes and another rerender is caused with
this.setState({
loadItemId: response.data
});
which initiates another AnotherItem rerender and expected result which is false.
Try removing state change in selectItem and you'll get desired result.
I'd suggest you read this article and try to structure your code differently.
EDIT
You can easily fix this with adding loader to your component:
selectItem = (id) => {
this.setState({
itemId: id,
loading: true
})
this.load(id);
}
load = (id) => {
axios.get
axios({
url: `https://jsonplaceholder.typicode.com/todos/${id}`,
method: "GET"
})
.then(response => {
this.setState({
loading: false,
loadItemId: response.data
});
})
.catch(error => {
console.log(error);
})
}
render() {
return (
<div >
<ul>
{this.state.items.map((item, index) =>
<Item
key={item.id}
item={item}
selectItem={this.selectItem}
/>
)
}
</ul>
{this.state.loading ? <span>Loading...</span> : <Details
itemId={this.state.itemId}
loadItemId={this.state.loadItemId}
/>}
</div>
)
}
This way, you'll rerender your Details component only when you have data fetched and no unnecessary rerenders will occur.
I'm running into a recurring issue in my code where I want to grab multiple pieces of data from a component to set as states, and push those into an array which is having its own state updated. The way I am doing it currently isn't working and I think it's because I do not understand the order of the way things happen in js and react.
Here's an example of something I'm doing that doesn't work: jsfiddle here or code below.
import React, {Component} from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = {
categoryTitle: null,
categorySubtitle: null,
categoryArray: [],
}
}
pushToCategoryArray = () => {
this.state.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
setCategoryStates = (categoryTitle, categorySubtitle) => {
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
})
this.pushToCategoryArray();
}
render() {
return (
<CategoryComponent
setCategoryStates={this.setCategoryStates}
categoryTitle={'Category Title Text'}
categorySubtitle={'Category Subtitle Text'}
/>
);
}
}
class CategoryComponent extends Component {
render() {
var categoryTitle = this.props.categoryTitle;
var categorySubtitle = this.props.categorySubtitle;
return (
<div onClick={() => (this.props.setCategoryStates(
categoryTitle,
categorySubtitle,
))}
>
<h1>{categoryTitle}</h1>
<h2>{categorySubtitle}</h2>
</div>
);
}
}
I can see in the console that I am grabbing the categoryTitle and categorySubtitle that I want, but they get pushed as null into this.state.categoryArray. Is this a scenario where I need to be using promises? Taking another approach?
This occurs because setState is asynchronous (https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly).
Here's the problem
//State has categoryTitle as null and categorySubtitle as null.
this.state = {
categoryTitle: null,
categorySubtitle: null,
categoryArray: [],
}
//This gets the correct values in the parameters
setCategoryStates = (categoryTitle, categorySubtitle) => {
//This is correct, you're setting state BUT this is not sync
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
})
this.pushToCategoryArray();
}
//This method is using the state, which as can be seen from the constructor is null and hence you're pushing null into your array.
pushToCategoryArray = () => {
this.state.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
Solution to your problem: pass callback to setState
setCategoryStates = (categoryTitle, categorySubtitle) => {
//This is correct, you're setting state BUT this is not sync
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
}, () => {
/*
Add state to the array
This callback will be called once the async state update has succeeded
So accessing state in this variable will be correct.
*/
this.pushToCategoryArray()
})
}
and change
pushToCategoryArray = () => {
//You don't need state, you can simply make these regular JavaScript variables
this.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
I think React doesn't re-render because of the pushToCategoryArray that directly change state. Need to assign new array in this.setState function.
// this.state.categoryArray.push({...})
const prevCategoryArray = this.state.categoryArray
this.setState({
categoryArray: [ newObject, ...prevCategoryArray],
)}
In the parent component, I receive data from the server and then map this data into a jsx format. Inside this mapping I have a child component and try to pass a value from state of parent to child as a property, however when I update state of this value, the render function for child is not executed.
Expected behavior: As a user I see a list of items. If I click on an item it should become as checked.
export class ReactSample extends React.Component {
constructor(props){
super(props);
this.state = {
items: [],
mappedItems: [],
selectedIds: [],
isSelected: false,
clickedTripId: null
};
this.toggleSelection = this.toggleSelection.bind(this);
}
componentWillMount(){
console.log("Component mounting")
}
toggleSelection (id, e) {
if(!_.includes(this.state.selectedIds, id)) {
this.setState((state) => ({selectedIds:
state.selectedIds.concat(id)}));
this.setState(() => ({clickedTripId: id}));
this.mapItems(this.state.items);
}
}
componentDidMount() {
const self = this;
MyService.getItems()
.then(res => {
self.setState(() => ({ items: res.allItems }));
self.setState(() => ({ mappedItems:
this.mapItems(res.allItems) }));
}
)
}
mapItems (items) {
return items.map(trip => {
return (
<li key={trip.id} onClick={(e) => (this.toggleSelection(trip.id,
e))}>
<span>{trip.title}</span>
<Tick ticked={this.state.clickedTripId}/>
<span className="close-item"></span>
</li>
);
});
}
getItems() {
}
render() {
return (
<div>
<a className="title">This is a react component!</a>
<Spinner showSpinner={this.state.items.length <= 0}/>
<div className="items-container">
<ul id="itemsList">
{this.state.mappedItems}
</ul>
</div>
</div>
);
}
}
export class Tick extends React.Component {
constructor(props) {
super(props);
}
render() {
console.log('RENDER');
return (<span className={this.props.ticked ? 'tick display' :
'tick hide' }></span>);
}
}
I see a couple issues.
In toggleSelection you aren't doing anything with the result of mapItems. This kind of bug would be much easier to avoid if you just remove mappedItems from state and instead just call mapItems within your render method.
The other issue is you are passing this.state.clickedTripId as the ticked property. I assume you meant to pass something more like this.state.clickedTripId === trip.id.
As Ryan already said, the problem was that mappedItems where not updated when toggleSelection was clicked. As it is obvious from the code mapItems returns data in jsx format. To update it I had to call this.setState({mappedItems: this.mapItems(this.state.items)}) which means that I call mapItems and then I assign the result to the state. In this case my list will be updated and Tick component will receive this.state.clickedItemId as a tick property. There is one more issue that needs to be done to make this code working:
this mapped list needs to be updated after this.state.clickedItemId is updated. The method setState is asynchronous which means that this.setState({mappedItems: this.mapItems(this.state.items)}) has to be called only after this.state.clickedItemId is updated. To achieve this, the setState method can receive a callback function as a second parameter. The code snippet is the following:
toggleSelection (id, e) {
if(!_.includes(this.state.selectedIds, id)) {
this.setState((state) => ({
clickedItemId: id,
selectedIds: state.selectedIds.concat(id)
}), () => this.setState({mappedItems: this.mapItems(this.state.items)}));
}
}
In this case, at the time the mapItems function is executed all data from the state that is needed here will be already updated:
mapItems (items) {
return items.map(item => {
return (
<li key={item.id} onClick={(e) => (this.toggleSelection(item.id, e))}>
<span>{item.title}</span>
<span>{this.state.clickedItemId}</span>
<Tick ticked={this.state.clickedItemId === item.id}/>
<span className="close-item"></span>
</li>
);
});
}
I want to update array value using index, is below code ok?
handleChange = index => e => {
const { rocket } = this.state // ['tesla', 'apple', 'google']
rocket[index] = e.target.value
this.setState({ rocket })
}
my jsx
<div>{rocket.map((val,i) => <input type="text" onChange={handleChange(i)} value={val} />)}</div>
I know it worked, but just to be sure it's ok to mutate the state like that.
It's not okay to mutate state this way.
The following line mutates the array in the current state in a way that can lead to bugs in your program particularly with components down the Component tree using that state.
This is because the state is still the same array.
rocket[index] = e.target.value
//console.log(this.state.rocket) and you see that state is updated in place
Always treat state as immutable
You can remedy this by writing.
const newRocket = [
...rocket.slice(0, index),
e.target.value,
...rocket.slice(index + 1)
]
This way a new array is created and components in the Component tree can be updated when React does a reconciliation.
Note that
The only way to mutate state should be through calls to Component.setState.
Now that you have a new array, you can update the component state like so:
this.setState({ rocket: newRocket })
Instead of changing existing value, you could use Array.prototype.splice().
The splice() method changes the contents of an array by removing existing elements and/or adding new elements.
var arr= ['A','B','E','D'];
arr.splice(2,1,'C')
console.log(arr)//The result will be ['A','B','C','D'];
.as-console-wrapper {max-height: 100% !important;top: 0;}
Stackblitz demo
CODE SNIPPET
class App extends Component {
constructor() {
super();
this.state = {
name: 'Demo using Array.prototype.slice()',
rocket: ['tesla', 'apple', 'google'],
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice'
};
}
handleChange(index, e) {
const { rocket } = this.state;
rocket.splice(index, 1, e.target.value)
this.setState({ rocket: [...rocket] }, () => {
//call back function of set state you could check here updated state
console.log(this.state.rocket)
});
}
render() {
return (
<div>
<b><a target="_blank" href={this.state.link}>{this.state.name}</a></b>
{
this.state.rocket.map((val, i) =>
<p key={i}>
<input type="text" onChange={(e) => { this.handleChange(i, e) }} value={val} />
</p>)
}</div>
);
}
}
render(<App />, document.getElementById('root'));