ReactJS remove child element created through count - javascript

I have two components, a TrackSection(the Parent element) which has a button that creates a TrackItem(child) every time it is clicked. The child elements are built through a variable numTracks which increments every time the button is clicked. The add button works fine but i'm having issues deleting a TrackItem from the array. I tried referencing the track_items directly but it won't let me.
I'm very new to React and Frontend development. Any other tips would be appreciated!
TrackSection.js
class TrackSection extends Component {
constructor(props) {
super(props);
this.state = {
numTracks: 0,
};
}
onAddTrack = () => {
this.setState({
numTracks: this.state.numTracks + 1,
});
};
onDeleteTrack = () =>{
//????
};
render() {
const track_items = [];
for (var i = 0; i < this.state.numTracks; i += 1) {
track_items.push(<TrackItem key={i} id={i} onDeleteTrack = {this.onDeleteTrack(i)}/>);
}
return (
<div>
<Button onClick={this.onAddTrack}>
+new track
</Button>
{track_items}
</div>
);
}
}
TrackItem.js
class TrackItem extends Component{
constructor(props){
super(props);
this.state = {
id: this.props.id,
name: '',
}
}
render(){
var onDeleteTrack = this.props.onDeleteTrack
return(
<Grid container direction="row">
<Grid item direction="column">
//Dummy
</Grid>
<button onClick={() => onDeleteTrack(this.props.id)}>Delete</button>
</Grid>
);
}}

Issue
You are using an array index as the React key, and the id. When you remove an element from the array you may remove it from the array, but since the items shift up to fill the "hole" now all the elements in the array have incorrect "id"/index values.
Solution
Don't use a mapped array index as a React key.
Example solution uses an incrementing id, as before, but also stores the array in state. This allows you to consistently increment the id key and retain a static id with each element.
class TrackItem extends Component {
constructor(props) {
super(props);
this.state = {
id: this.props.id,
name: ""
};
}
render() {
var onDeleteTrack = this.props.onDeleteTrack;
return (
<Grid container direction="row">
<Grid item direction="column">
//Dummy
</Grid>
<button onClick={() => onDeleteTrack(this.props.id)}>Delete {this.props.id}</button>
</Grid>
);
}
}
class TrackSection extends Component {
constructor(props) {
super(props);
this.state = {
tracks: [],
id: 0,
};
}
onAddTrack = () => {
this.setState(prevState => ({
tracks: [...prevState.tracks, prevState.id],
id: prevState.id + 1,
}));
};
onDeleteTrack = (id) =>{
this.setState(prevState => ({
tracks: prevState.tracks.filter(el => el !== id)
}))
};
render() {
return (
<div>
<button onClick={this.onAddTrack}>
+new track
</button>
{this.state.tracks.map(track => (
<TrackItem key={track} id={track} onDeleteTrack = {this.onDeleteTrack}/>
))}
</div>
);
}
}

Be careful about doing too much logic in your render function, as your current solution would recreate all the TrackItem's every time you add a new item. So React can't do optimization magic.
Second remark, now you are just having a counter, so removing a element in the middle would probably not have the effect you are looking for. I assume the track items will be having some data to them. Like name, etc. So just store those values in the state and render each item.
Here is a sample solution, modify for your needs:
class TrackSection extends Component {
constructor(props) {
super(props);
this.state = {
tracks: []
};
}
onAddTrack = () => {
// Probably not the best way to create a id
const randomId = Math.random().toString();
const newTrack = {
id: randomId,
name: "Some name" + randomId
};
const newTracks = [
// the tracks we allready have added
...this.state.tracks,
// add a new track to the end
newTrack
];
// Replace state
this.setState({
tracks: newTracks
});
};
onDeleteTrack = (id) => {
// Keeps all tracks that don't match 'id'
const tracksWithOutDeleted = this.state.tracks.filter(
(track) => track.id !== id
);
// Replace the tracks, so now its gone!
this.setState({
tracks: tracksWithOutDeleted
});
};
render() {
return (
<div>
<button onClick={this.onAddTrack}>+new track</button>
{
// Loop over tracks we have in state and render them
this.state.tracks.map((track) => {
return (
<TrackItem
id={track.id}
name={track.name}
onDeleteTrack={this.onDeleteTrack}
/>
);
})
}
</div>
);
}
}
And the TrackItem.js:
class TrackItem extends Component {
render() {
const { onDeleteTrack, id, name } = this.props;
return (
<>
<button onClick={() => onDeleteTrack(id)}>Delete {name}</button>
</>
);
}
}

Related

Assigning each button it's own individual state count

My code below shows my current component design. This is a counter component which is responsible for incrementing a counter for the respective array item and also for adding the clicked item to the cart. I am trying to figure out if there is some way in which I can assign each array item within the items array to its own state count value. Currently, the screen shows four array items, with each one having a button next to it and also a count. When clicking the increment button for any particular item, the state count for all buttons is updated and rendered, which is not what I want. I have tried to assign each button it's own state count in several ways, but haven't been able to figure out the right way. I would like to somehow bind a state count value to each button so that each one has it's individual state count.I would really appreciate if someone can provide some tips or insight as I dont know of a way to isolate the state count for each button and make it unique so that when one value's button is clicked, only the state count for that particular button (located next to the increment button) is updated and not the others.
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
cart: [],
};
}
handleIncrement = (e) => {
this.setState({
count: this.state.count + 1,
cart: [...this.state.cart, e.target.value],
});
};
render() {
const listItems = this.props.items.map((item) => (
<li key={item.id}>
{item.value}
<button onClick={this.handleIncrement}>+</button>
{this.state.count}
</li>
));
return (
<div>
{listItems}
</div>
);
}
}
What I did here is I remove the constructor, update Counter component props, update the event on how to update your cart in Example component, adjusted the Counter component, for the Cart component, I added componentDidMount and shouldComponentUpdate make sure that the component will re-render only when props listArray is changing. Here's the code.
class Example extends React.Component {
state = {
cart: [],
items: [
{ id: 1, value: "L1" },
{ id: 2, value: "L2" },
{ id: 3, value: "L3" },
{ id: 4, value: "L4" }
]
}
render() {
const { cart } = this.state
return (
<div>
<h1>List</h1>
{ items.map(
({ id, ...rest }) => (
<Counter
key={ id }
{ ...rest }
cart={ cart }
onAddToCard={ this.handleAddCart }
/>
)
) }
</div>
)
}
handleAddCart = (item) => {
this.setState(({ items }) => ([ ...items, item ]))
}
}
class Counter extends React.Component {
state = {
count: 0
}
handleIncrement = () => {
this.setState(({ count }) => ({ count: count++ }))
}
render() {
const { count } = this.state
const { cart, value } = this.props
return (
<div>
{ value }
<span>
<button onClick={ this.handleIncrement }>+</button>
{ count }
</span>
<Cart listArray={ cart } />
</div>
)
}
}
class Cart extends React.Component {
state = {
cart: []
}
addTo = () => (
<div>List: </div>
)
componentDidMount() {
const { cart } = this.props
this.setState({ cart })
}
shouldComponentUpdate({ listArray }) {
return listArray.length !== this.state.cart.length
}
render() {
return (
<div>
<ListFunctions addClick={ this.addTo } />
</div>
)
}
}
const ListFunctions = ({ addClick }) => (
<div>
<button onClick={ addClick }>Add To List</button>
</div>
)
If you want to add to the list of items without rendering the button, you can add a custom property to mark that it is a custom addition:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [
{ id: 1, value: "L1" },
{ id: 2, value: "L2" },
{ id: 3, value: "L3" },
{ id: 4, value: "L4" },
]
}
}
addToItems = items => {
this.setState({
items,
});
}
render() {
var cartArray = [];
return (
<div>
<h1>List</h1>
{this.state.items.map((item) =>
<Counter
key={item.id}
value={item.value}
id={item.id}
custom={item.custom}
cart={cartArray}
addToItems={this.addToItems}
items={this.state.items}
/>
)}
</div>
);
}
}
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
handleIncrement = () => {
this.setState({
count: this.state.count + 1,
});
this.props.cart.push(this.props.value);
};
addTo = () => {
const { items } = this.props;
let lastId = items.length;
lastId++;
this.props.addToItems([
...items,
{
id: lastId,
value: `L${lastId}`,
custom: true,
}]);
};
render() {
return (
<div>
{this.props.value}
{
!this.props.custom &&
(
<span>
<button onClick={this.handleIncrement}>+ </button>
{this.state.count}
</span>
)
}
<Cart addTo={this.addTo} />
</div>
);
}
}
class Cart extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<ListFunctions
addClick={this.props.addTo}
/>
</div>
);
return null;
}
}
const ListFunctions = ({ addClick}) => (
<div>
<button onClick={addClick}>Add To List</button>
</div>
);
// Render it
ReactDOM.render(
<Example />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>

React-Native How to have a different state for each item

I have a component where when I click on an icon, I execute a function that modify a state and then i can check the state and modify the icon. In that comonent, I am mapping datas and it renders several items.
But when I click on one icon all the icons of the components change too.
Here is the code for the component
export default class DiscoveryComponent extends Component {
constructor(props) {
super(props)
this.state = {
starSelected: false
};
}
static propTypes = {
discoveries: PropTypes.array.isRequired
};
onPressStar() {
this.setState({ starSelected: !this.state.starSelected })
}
render() {
return (
this.props.discoveries.map((discovery, index) => {
return (
<Card key={index} style={{flex: 0}}>
<CardItem>
<TouchableOpacity style={[styles.star]}>
<Icon style={[styles.iconStar]} name={(this.state.starSelected == true)?'star':'star-outline'} onPress={this.onPressStar.bind(this)}/>
</TouchableOpacity>
</CardItem>
</Card>
)
})
);
}
}
And here is the code for my screen that uses the component
export default class DiscoveryItem extends Component {
constructor(props) {
super(props);
this.state = {
discoveries: [],
loading: true
};
}
componentDidMount() {
firebase.database().ref("discoveries/").on('value', (snapshot) => {
let data = snapshot.val();
let discoveries = Object.values(data);
this.setState({discoveries: discoveries, loading: false});
});
}
render() {
return (
<Container>
<Content>
<DiscoveryComponent discoveries={this.state.discoveries} />
</Content>
</Container>
)
}
}
Your initiation is correct but you are missing INDEX of each item. Inside this.onPressStar() method check if item's index = currentItem. Also don't forget to set item id = index onpress.
I hope this has given you idea how to handle it.
You have to turn your stars into an Array and index them:
change your constructor:
constructor(props) {
super(props)
this.state = {
starSelected: []
};
}
change your onPressStar function to :
onPressStar(index) {
this.setState({ starSelected[index]: !this.state.starSelected })
}
and your icon to
<Icon style={[styles.iconStar]} name={(this.state.starSelected[index] == true)?'star':'star-outline'} onPress={()=>this.onPressStar(index)}/>
Well, the problem is that you have a single 'starSelected' value that all of your rendered items in your map function are listening to. So when it becomes true for one, it becomes true for all.
You should probably maintain selected state in the top level component, and pass down the discovery, whether its selected, and how to toggle being selected as props to a render function for each discovery.
export default class DiscoveryItem extends Component {
constructor(props) {
super(props);
this.state = {
discoveries: [],
selectedDiscoveries: [] // NEW
loading: true
};
}
toggleDiscovery = (discoveryId) => {
this.setState(prevState => {
const {selectedDiscoveries} = prevstate
const discoveryIndex = selectedDiscoveries.findIndex(id => id === discoveryId)
if (discoveryIndex === -1) { //not found
selectedDiscoveries.push(discoveryId) // add id to selected list
} else {
selectedDiscoveries.splice(discoveryIndex, 1) // remove from selected list
}
return {selectedDiscoveries}
}
}
componentDidMount() {
firebase.database().ref("discoveries/").on('value', (snapshot) => {
let data = snapshot.val();
let discoveries = Object.values(data);
this.setState({discoveries: discoveries, loading: false});
});
}
render() {
return (
<Container>
<Content>
{
this.state.discoveries.map(d => {
return <DiscoveryComponent key={d.id} discovery={d} selected={selectedDiscoveries.includes(d.id)} toggleSelected={this.toggleDiscovery} />
//<DiscoveryComponent discoveries={this.state.discoveries} />
</Content>
</Container>
)
}
}
You can then use your DiscoveryComponent to render for each one, and you're now maintaining state at the top level, and passing down the discovery, if it is selected, and the toggle function as props.
Also, I think you may be able to get snapshot.docs() from firebase (I'm not sure as I use firestore) which then makes sure that the document Id is included in the value. If snapshot.val() doesn't include the id, then you should figure out how to include that to make sure that you use the id as both key in the map function as well as for the selectedDiscoveries array.
Hope that helps
It works now, thanks.
I've made a mix between Malik and Rodrigo's answer.
Here is the code of my component now
export default class DiscoveryComponent extends Component {
constructor(props) {
super(props)
this.state = {
tabStarSelected: []
};
}
static propTypes = {
discoveries: PropTypes.array.isRequired
};
onPressStar(index) {
let tab = this.state.tabStarSelected;
if (tabStar.includes(index)) {
tabStar.splice( tabStar.indexOf(index), 1 );
}
else {
tabStar.push(index);
}
this.setState({ tabStarSelected: tab })
}
render() {
return (
this.props.discoveries.map((discovery, index) => {
return (
<Card key={index} style={{flex: 0}}>
<CardItem>
<Left>
<Body>
<Text note>{discovery.category}</Text>
<Text style={[styles.title]}>{discovery.title}</Text>
</Body>
</Left>
<TouchableOpacity style={[styles.star]}>
<Icon style={[styles.iconStar]} name={(this.state.tabStarSelected[index] == index)?'star':'star-outline'} onPress={()=>this.onPressStar(index)}/>
</TouchableOpacity>
</CardItem>
</Card>
)
})
);
}
}

Change Specific Property of the State

I am learning ReactJS and needless to say I am an absolute beginner! I am trying to change a specific property in the array of objects which belongs to state. Every object has two properties: name and active. active values are false by default. When I click on the item, I want to change this item's active value to true.
My array is shown inside of the list element and every list element has onClick={() => props.onChangeSelected(lang.name)} method. onChangeSleceted method goes to handleChangeSelected(name) function, however, I couldn't figure out what to write inside of this function.
class Loading extends React.Component {
constructor(props) {
super(props);
this.state = {
text: 'Loading'
};
}
componentDidMount() {
const stopper = this.state.text + '...';
this.interval = window.setInterval(() => {
this.state.text === stopper
? this.setState(() => ({ text: 'Loading' }))
: this.setState((prevState) => ({ text: prevState.text + '.' }))
}, 300)
}
componentWillUnmount() {
window.clearInterval(this.interval);
}
render() {
return (
<p>
{this.state.text}
</p>
)
}
}
function LanguageList (props) {
return (
<div>
<h3>Choose your favorite:</h3>
<ul>
{props.list.map((lang) => (
<li key={lang.name} onClick={() => props.onChangeSelected(lang.name)}>
<span>{lang.name}</span>
</li>
))}
</ul>
</div>
)
}
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
languages: [
{
name: 'all',
active: true
},
{
name: 'javascript',
active: false
},
{
name: 'ruby',
active: false
},
{
name: 'python',
active: false
}
]
}
this.handleChangeSelected = this.handleChangeSelected.bind(this)
}
handleChangeSelected(name) {
this.setState((currentState) => {
const lang = currentState.languages.find((lang) => lang.name === name)
return {}
})
}
render() {
return (
<div>
<LanguageList
list={this.state.languages}
onChangeSelected={this.handleChangeSelected}
/>
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('app')
)
</script>
You can do it in a number of ways. All you need to make sure is that you aren't mutating the original state array
handleChangeSelected(name) {
this.setState((currentState) => {
return { languages: currentState.languages.map((lang) => {
if(lang.name === name) {
return {...lang, active: true};
}
return lang;
});
})
}
Try this?
handleChangeSelected(name){
// Find matching element in state
var temp = this.state.languages;
for (var i = 0; i < temp.length; i++){
if (temp[i]["name"] === name){
temp[i]["active"] = true;
}
}
this.setState({
languages: temp
});
}
As listed in the React docs, they recommend creating a new object when calling the setState function. This is of course talking about the updater function syntax (this.setState((prevState, props) => {return {...};});), which I assume the same logic is applied to the syntax used above (passing an object into set state)
The first argument [to setState] is an updater function with the signature:
(prevState, props) => stateChange
(prevState, props) => stateChange prevState is a reference to the
previous state. It should not be directly mutated. Instead, changes
should be represented by building a new object based on the input from
prevState and props.

react js delete handler removes the 1st index in the array

The problem is any item button click will delete the 1st index item in the array.
I looked at these resources on handling deleting an item in an array in react.
How to remove item in todo list using React
Removing element from array in component state
React Binding Patterns
I've tried changing how my handler is called in TodoList and TodoItemLIst and that causes the handler not to fire on click. I've tried different methods of binding the handler - adding a param has no effect on it -bind(this) breaks it & isn't necessary because I'm using a function.
I've tried setting state different ways using a filter method. No change happens...
this.setState((prevState) => ({
todoItems: prevState.todoItems.filter(i => i !== index)
}));
I'm not understanding where/what the problem is.
class App extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
listItemValue: props.value || '',
todoItems: [
{id: _.uniqueId(), item: 'Learn React.'},
{id: _.uniqueId(), item: 'Improve JS skills.'},
{id: _.uniqueId(), item: 'Play with kittens.'}
]
};
}
handleChange = (event) => {
let value = event.target.value;
this.setState({
value: this.state.value,
listItemValue: value
});
}
handleSubmit = (event) =>{
event.preventDefault();
this.setState({
value: '',
listItemValue: ''
});
}
addTodoItem = () => {
let todoItems = this.state.todoItems.slice(0);
todoItems.push({
id: _.uniqueId(),
item: this.state.listItemValue
});
this.setState(prevState => ({
todoItems: [
...prevState.todoItems,
{
id: _.uniqueId(),
item: this.state.listItemValue
}]
}))
};
deleteTodoItem = (index) => {
let todoItems = this.state.todoItems.slice();
todoItems.splice(index, 1);
this.setState({
todoItems
});
}
render() {
return (
<div className="App">
<h1>Todo List</h1>
<TodoListForm name="todo"
onClick={ ()=>this.addTodoItem() }
onSubmit={ this.handleSubmit }
handleChange={ this.handleChange }
value={ this.state.listItemValue } />
<TodoList onClick={ ()=>this.deleteTodoItem() }
todoItems={ this.state.todoItems }/>
</div>
);
}
}
const TodoList = (props) => {
const todoItem = props.todoItems.map((todo) => {
return (
<TodoListItem onClick={ props.onClick }
key={ todo.id }
id={ todo.id }
item={ todo.item }/>
);
});
return (
<ul className="TodoList">
{todoItem}
</ul>
);
}
const TodoListItem = (todo, props) => {
return (
<li className="TodoListItem">
<div className="TodoListItem__Item">{todo.item}
<span className="TodoListItem__Icon"></span>
<button onClick={ todo.onClick }
type="button"
className="TodoListItem__Btn">×</button>
</div>
</li>
)
};
In the deleteTodoItem method, try just
let todoItems = this.state.todoItems.slice(0, -1);
and remove the call to splice().

How can I use `setState` with objects nested in an array in React JS?

With this code, I am able to successfully use setState on a simple object – when I click on "Joey" the name changes to "Igor".
class Card extends React.Component {
myFunc = () => {this.props.change('Igor')};
render() {
return (
<p onClick={this.myFunc}>{this.props.name}</p>
)
}
}
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = { name: "Joey" }
}
toggle = (newname) => {
this.setState((prevState, props) => ({
name: newname
}));
}
render() {
return (
<Card change={this.toggle} name={this.state.name} />
);
}
}
But with this code, which has multiple objects nested in an array, setState is either not able to change each name to "Igor" or it must be modified in some way.
class Card extends React.Component {
myFunc = () => {this.props.change('Igor')};
render() {
return (
<p onClick={this.myFunc}>{this.props.name}</p>
)
}
}
class Parent extends React.Component {
constructor(props) {
super(props)
this.state = {
names: [
{
name: "Joey"
},
{
name: "Sally"
},
{
name: "Billy"
},
]
}
}
toggle = (newname) => {
this.setState((prevState, props) => ({
// what can I put here to change the name I click on to "Igor"
}));
}
render() {
const names = this.state.names.map((name, index) => (
<Card key={index} change={this.toggle} {...name} />
))
return (
<div>
{names}
</div>
);
}
}
Even though I know this is not how setState works, I tried to access name by passing index and then writing this.state.names[index].name: newname. No surprises here, it didn't work.
I have researched and cannot find similar questions on SO about this although I have found a lot of mentions with regards to immutability helpers. But I am still not sure if that is the way to go.
What is the best way to use setState to modify objects nested in an array?
Have modified your code and the working example can be found here.
The changes can be found here:
toggle = (index, newname) => {
this.setState((prevState, props) => ({
// Return new array, do not mutate previous state.
names: [
...prevState.names.slice(0, index),
{ name: newname },
...prevState.names.slice(index + 1),
],
}));
}
render() {
const names = this.state.names.map((name, index) => (
// Need to bind the index so callback knows which item needs to be changed.
<Card key={index} change={this.toggle.bind(this, index)} {...name} />
))
return (
<div>
{names}
</div>
);
}
The idea is that you need to pass the index into the callback function via .bind, and return a new state array with the modified name. You need to pass the index so that the component knows which object to change the name to newname.
I would use this for the toggle method:
toggle = (nameYouWantChanged, nameYouWantItChangedTo) => {
this.setState({
names: this.state.names.map(obj =>
obj.name === nameYouWantChanged
? { name: nameYouWantItChangedTo }
: obj
)
})
}

Categories