How to use setState to splice into an array in the state? - javascript

My state.events is in array that is made up of the component instance: EventContainer.
I want my setState to place a new EventContainer in the state.events array. However, I want that EventContainer to go in the index immediately after the specific EventContainer that made the setState call.
I'm looking for help with making the adjustments necessary to my approach or, if my entire approach is bad, a recommendation on how to go about this. Thank you very much.
I'm developing an itinerary builder which is made up of rows/EventContainers that represent an activity on a given day.
Each EventContainer has a button that needs to offer the user the ability to onClick an additional row immediately after that EventContainer.
class DayContainer extends React.Component {
constructor(props){
super(props);
this.state = {
events: [],
};
this.pushNewEventContainerToState = this.pushNewEventContainerToState.bind(this);
}
pushNewEventContainerToState (index){
let newEvent = <EventContainer />;
this.setState(prevState => {
const events = prevState.events.map((item, j) => {
if (j === index) {
events: [...prevState.events.splice(index, 0, newEvent)]
}
})
})
}
render(){
return (
<>
<div>
<ul>
{
this.state.events === null
? <EventContainer pushNewEventContainerToState= .
{this.pushNewEventContainerToState} />
: <NewEventButton pushNewEventContainerToState={this.pushNewEventContainerToState} />
}
{this.state.events.map((item, index) => (
<li
key={item}
onClick={() =>
this.pushNewEventContainerToState(index)}
>{item}</li>
))}
</ul>
</div>
</>
)
}
}
My goal in setState was to splice newEvent into this.state.events immediately after the index (the parameter in pushNewEventContainerToState function).
I'm getting this error but I'm guessing there's more going on than just this: Line 23:22: Expected an assignment or function call and instead saw an expression no-unused-expressions.

I can see at least 2 issues with the code.
- Splice will mutate the array in place
- You are not returning the updated state.
You can instead use slice to build the new array.
pushNewEventContainerToState(index) {
let newEvent = < EventContainer / > ;
this.setState(prevState => {
const updatedEvents = [...prevState.events.slice(0, index], newEvent, ...prevState.events.slice(index + 1];
return {
events: updatedEvents
})
})
}

As I'm fairly new to coding, it took me awhile but I was able to compile the full answer. Here is the code, below. Below that, I explain, point by point, what the problem was and how the updated code addresses that.
class DayContainer extends React.Component {
constructor(props){
super(props);
this.state = {
events: [{key:0}],
};
this.pushNewEventContainerToState = this.pushNewEventContainerToState.bind(this);
}
pushNewEventContainerToState(index) {
let newEvent = {key: this.state.events.length};
this.setState(prevState => {
let updatedEvents = [...prevState.events.slice(0, index + 1), newEvent, ...prevState.events.slice(index + 1)];
return {
events: updatedEvents
};
})
}
render(){
return (
<>
<div>
<ul>
{this.state.events.map((item, index) => (
<li key={item.key}>
< EventContainer pushNewEventContainerToState={() => this.pushNewEventContainerToState(index) } / >
</li>
))}
</ul>
</div>
</>
)
}
}
Setup
Starting with state.events, instead of starting with an empty array, I'm starting with one object, including a key starting at 0, because I always want the user to start with one EventContainer.
Regarding pushNewEventContainerToState, #Sushanth made a great recommendation. Please refer directly to that function in my latest code. The refinement I made has to do with the way I separate the EventContainer being passed to this.state.events. I've moved the EventContainer from pushNewEventContainerToState down to the render() element. I've given it a prop of key={item.key} and wrapped the component instance in a li. The very first EventContainer will have a key of 0 (see state.events[0]). Now, each new EventContainer passed to state.events will have a key that's based off the latest .length() of the state.events array (refer to the latest value of the let newEvent variable in pushNewEventContainerToState).
All of that allowed me to fix a big problem I was facing: I needed the newest EventContainer to be placed in the index immediately after the index of the EventContainer calling pushNewEventContainerToState. The main reason this was happening was because I wasn't properly passing the index to the EventContainer inside of render(). Now that I have the actual EventContainer there, I can pass it a prop in the right manner (please refer EventContainer's prop in render). Now I'm calling pushNewEventContainerToState with the correct index.

Related

Why is the first element getting removed too?

So I am new to javascript and I tried making a todo list. This works well with adding elements. The issue is when I am removing some item, the first one gets removed too, why is it so? I know I am missing a small thing and this may be really basic but I am not able to find out what that is.
const App1 = () => {
const [item, updatedItem]=useState('');
const [Items, setItems]=useState([]);
function inputEvent(event) {
updatedItem(event.target.value);
}
const addItem = (event) => {
event.preventDefault();
setItems((prev) => {
return[
...prev,
item
]
});
updatedItem('');
}
let key=0;
return(<>
<div className='back'>
<div className='list'>
<header>ToDo List</header>
<form onSubmit={addItem}>
<input type='text' placeholder='Add an item' value={item} onChange={inputEvent}/>
<button type='submit'>+</button>
</form>
<div className='items'>
<ol>
{Items.map((val) => <li><button id={key++} onClick={(event) => {
setItems((Items) => {
return Items.filter((val, index) => {
if(index!==Number(event.target.id)){
return index;
}
}
);
});
key=0;
}}>x</button>{val}</li>)}
</ol>
</div>
</div>
</div>
</>);
You return the index in your filter, expecting this to always be true, yet 0 (the index of the first element) is a falsy value.
Try this instead:
return Items.filter((_val, index) => index !== Number(event.target.id));
Some unrelated code-quality notes:
In React, you should always set a key prop on each element when looping through them, rather than id.
map has a second argument, index, which it passes into the callback --- you don't have to keep track of this yourself with e.g. key++ etc.
If you use map's index parameter, then you can pass that directly into your filter rather than using Number(event.target.id), which is not very idiomatic in React.
If you don't use an argument of a callback, it's a good idea to prefix it with a _ (like I've done with _val here), to make it explicit that you're not using it.
Your filter callback should return a flag. index is a number. When treated as a flag, 0 is false (more on MDN). Instead:
return Items.filter((val, index) => index !== Number(event.target.id));
However, your code is returning an array of li elements without setting key on them (see: keys), which React needs in order to manage that list properly (you should be seeing a warning about it in devtools if you're using the development version of the libs, which is best in development). You can't use the mechanism you're using now for keys when doing that, it will not work reliably (see this article linked by the React documentation). Instead, assign each Todo item a unique ID when you create it that doesn't change, and use that as the key (and as the value to look for when removing the item):
// Outside the component:
let lastId = 0;
// Inside the component:
const addItem = (event) => {
event.preventDefault();
setItems((prev) => {
return [
...prev,
{text: item, id: ++lastId}
];
});
updatedItem("");
};
// Add a remove function:
const removeItem = ({currentTarget}) => {
const id = +currentTarget.getAttribute("data-id"); // Get the ID, convert string to number
setItems(items => items.filter(item => item.id !== id));
};
// When rendering:
{Items.map((item) => <li key={item.id}><button data-id={item.id} onClick={removeItem}>x</button>{item.text}</li>)}
In some cases it may be useful to use useCallback to memoize removeItem to avoid unnecessary rendering, but often that's overkill.

How to Delete individual items from list in react

I have built a simple ToDo App. Rending the tasks from the form input is working fine, but I am unable to delete the tasks when clicked on Delete button.
export class TodoList extends Component {
constructor(props) {
super(props)
this.state = {
task:'',
items:[]
}
}
onChangeHandler=(e)=>{
this.setState({
[e.target.name]: e.target.value
})
}
addItem=(e)=>{
e.preventDefault()
if (this.state.task!==""){
this.setState({
items:[...this.state.items,this.state.task],
task:''
})
}
}
removeItem=(index)=>{
const remainingItems = this.state.items.filter(j => {
return j !== index
})
this.setState({
items: remainingItems
})
};
render() {
return (
<div>
<form>
<input type='text' name="task"onChange={this.onChangeHandler} value={this.state.task} placeholder='Enter Task'/>
<button type='submit' onClick={this.addItem}>Add Task</button>
</form>
<Lists items={this.state.items}
delete={this.removeItem}/>
</div>
)
}
}
export class Lists extends Component {
removeItems=(index)=>{
this.props.delete(index)
}
render() {
return (
<div>
{this.props.items.map((item,index)=>
<li className="Lists" key={index}>{item}
<button type='button' onClick={this.removeItems(index)}>Remove</button>
</li>)}
</div>
)
}
}
Do you even happen to have any items to delete here or the list comes up empty? Delete function itself looks fine but you have couple of problems here.
Don't use index as a key. In case you're reordering or deleting (which you are doing) an array of items, you can run into a lot of issues. Here's a good article: https://medium.com/#vraa/why-using-an-index-as-key-in-react-is-probably-a-bad-idea-7543de68b17c
Probably the error is with this since you're deleting key which, since it's an iterator, is reassigned to another element when array repopulates itself. Change iterator to some other unique identifier for each element.
You're calling removeItems method as soon as it's set. If you have invoked methods (with ()) inside return of render, it will be executed immediately on each refresh. That's why I'm asking do you have anything to delete at all since, if delete function is okay written, this would probably delete all items as soon as they are added.
Best method would be to use dataset. To each element you can add dataset like this:
data-item-id={some-id} and you can fetch it inside your method from the fired event like this const clickedId = event.currentTarget.dataset.someId. Note that dataset in the element must be written like-this, and it's rewritten automatically when fetching it into camelCase (likeThis). Then you can use this index to target the element you want inside the array and delete it.
Note that the iterator issue still applies, and you need a different unique identifier.
Let me know if you need further explanation.
You can delete the current item using splice method.
removeItem = index=> {
let newArray = this.state.items;
newArray.splice(index, 1);
this.setState({
items: newArray
});
};
It would be better to use onClick for removing item like this :
<button type='button' onClick={()=>this.removeItems(index)}>Remove</button>
Hope this helps.
I prefer to pass item that i would like to remove, index can be decieving becouse it changes.
Find index by unique key, i use item.id as unique key.
removeItem = item => {
const items = this.state.items;
// if using lodash i use findIndex
const index = _.findIndex(items, i => i.id === item.id)
// if plain js
const index = items.findIndex(i => i.id === item.id)
items.splice(index, 1);
this.setState({
items
});
};

Component List don't update state after deleting one item

I have an issue trying to update a list of stateful components created with by mapping a list of strings. The issue is showing when i remove one of this components by slicing the array to remove an element by its index.
Every component has his own state, that im fetching from an API. the problems is that when i remove an element of the array, the state of next component overlaps the one that i deleted Component.
My code looks something similar to this:
class MyDashboard extends React.Component {
constructor(props){
super(props);
this.state = {
activeItems: [0,1,2,3],
}
this.removeItem = this.removeItem.bind(this);
}
removeItem(indx){
let tempItems= this.state.activeItems;
tempItems.splice(indx,1);
this.setState({activeItems:tempItems});
}
render(){
let conditionalDash;
let conditionalComponent;
let temporalArray = this.state.activeEntries.map((entry , i) => {
return (<MyDash key={i} index {i}/> removeItem={this.removeItem});
});
render(){
return (
<div id='dashContainer'>
{temporalArray}
</div>
)
}
}
In my MyDashComponent i have a something like this:
class MyDash extends React.Component{
constructor(props){
super(props);
this.state={
fetchedData:null,
}
}
componentDidMount(){
API.fetchData(this.props.index).then(response => {
this.setState({fetchData:response.data})
)
}
render(){
return(
<div> {this.props.index} {this.state.fetchedData}</div>
)
}
}
Is there something that i'm missing?
The behavior that i'm getting is that when the i remove the this.state.activeItems[2] the state of this element is the same as the previous component. I was expecting that the state of the element[2] will be the same state that has the element[3].
Edit:
Something that i forget to tell, is that the props of MyDash component are correct, is just the state that doesnt belong to the component, it is from the deleted component.
Thanks for reading and i hope that somebody can help me with this.
I found the bug it was that the key of the list that i was using, it was the index of the map method, i read that it has to be a unique key. Luckily this fixed the render action and the state doesnt overlap anymore.
Who have mixed the behaviour or slice and splice
slice returns you a new array whereas splice modifies the existing one
According to MDN docs:
splice: The splice() method changes the contents of an array by
removing existing elements and/or adding new elements.
Syntax: array.splice(start, deleteCount)
slice: The slice() method returns a shallow copy of a portion of an
array into a new array object selected from begin to end (end not
included). The original array will not be modified.
syntax:
arr.slice()
arr.slice(begin)
arr.slice(begin, end)
You might change your code to
removeItem(indx){
let tempItems= this.state.activeItems;
tempItems.splice(indx,1);
this.setState({ activeItems:tempItems });
}
Also you shouldn't mutate the state directly, you should create a copy of the state array and then update it.
removeItem(indx){
let tempItems= [...this.state.activeItems]; // this is do a shallow copy, you could use something else depending on your usecase
tempItems.splice(indx,1);
this.setState({ activeItems:tempItems });
}
You can also use Array.prototype.filter to remove the item:
removeItem(idx) {
this.setState({
activeItems: this.state.activeItems.filter((_, index) => index !== idx)
});
}
or
removeItem(idx) {
this.setState(prevState => ({
activeItems: prevState.activeItems.filter((_, index) => index !== idx)
}));
}

React 16 forces component re-render while returning array

Assume I have the next piece of code inside the React component
removeItem = (item) => {
this.items.remove(item) // this.items -> mobx array
}
renderItem = (item, index) => {
var _item = undefined
switch (item.type) {
case "header":
_item = <Header key={item.id} onRemove={() => this.removeItem(item)} />
// a few more cases
// note that item.id is unique and static
}
// return _item -> works fine
return [
_item,
this.state.suggested
? <Placeholder key={-item.id} />
: null
]
}
render() {
return (
<div>
{this.items.map((item, i) => renderItem(item))}
</div>
)
}
Also assume that inside each of item I have a button that triggers onRemove handler with click. And each component has textarea where user can enter his text.
Obviously, when user enters text inside item's textarea, it should be saved until item will be removed.
The problem is when I remove some item, each item that goes after the removed one is being remounted (edited for Vlad Zhukov). It happens only when I return an array from renderItem(...) (I mean, when I return only item, this problem doesn't happen).
My question: is this a bug, or it's a feature? And how can I avoid it (desirable without wrapping item and Placeholder with another React child)?
UPDATED
I tried rewrite renderItem(...) the next way:
renderItem = (item, index) => {
var Item = undefined
switch (item.type) {
case "header":
Item = Header
// a few more cases
// note that item.id is unique and static
}
// return _item -> works fine
return [
<Item key={item.id} onRemove={() => this.removeItem(item)} />,
this.state.suggested
? <Placeholder key={-item.id} />
: null
]
}
And it still causes the problem.
Rerendering is absolutely fine in React and can be considered the main feature. What happens in your case is components remount when you make changes to an array of elements when these elements have no key props.
Have a look at this simple example. As you can see rerendering components has no difference but removing the first element will clear values of inputs below.
You've got 2 options:
Use a component instead of an array and set key to it (see an example). There is really no reason not to.
Remove all keys. The reason why it works is because React internally already uses keys for elements. However I wouldn't suggest this as it doesn't look reliable enough to me, I'd prefer to control it explicitly.

What are the benefits of immutability?

I'm using React to render long scrollable list of items (+1000). I found React Virtualized to help me with this.
So looking at the example here I should pass down the list as a prop to my item list component. What's tripping me up is that in the example the list is immutable (using Immutable.js) which I guess makes sense since that's how the props are supposed to work - but if I want to make a change to a row item I cannot change its state since the row will be rerendered using the list, thus throwing out the state.
What I'm trying to do is to highlight a row when I click it and have it still be highlighted if I scroll out of the view and back into it again. Now if the list is not immutable I can change the object representing the row and the highlighted row will stay highlighted, but I'm not sure that's the correct way to do it. Is there a solution to this other than mutating the props?
class ItemsList extends React.Component {
(...)
render() {
(...)
return(
<div>
<VirtualScroll
ref='VirtualScroll'
className={styles.VirtualScroll}
height={virtualScrollHeight}
overscanRowCount={overscanRowCount}
noRowsRenderer={this._noRowsRenderer}
rowCount={rowCount}
rowHeight={useDynamicRowHeight ? this._getRowHeight : virtualScrollRowHeight}
rowRenderer={this._rowRenderer}
scrollToIndex={scrollToIndex}
width={300}
/>
</div>
)
}
_rowRenderer ({ index }) {
const { list } = this.props;
const row = list[index];
return (
<Row index={index} />
)
}
}
class Row extends React.Component {
constructor(props) {
super(props);
this.state = {
highlighted: false
};
}
handleClick() {
this.setState({ highlighted: true });
list[this.props.index].color = 'yellow';
}
render() {
let color = list[this.props.index].color;
return (
<div
key={this.props.index}
style={{ height: 20, backgroundColor: color }}
onClick={this.handleClick.bind(this)}
>
This is row {this.props.index}
</div>
)
}
}
const list = [array of 1000+ objects];
ReactDOM.render(
<ItemsList
list={list}
/>,
document.getElementById('app')
);
If you only render let's say 10 out of your list of a 1000 at a time, then the only way to remember highlighted-flag, is to store it in the parent state, which is the list of 1000.
Without immutability, this would be something like:
// make a copy of the list - NB: this will not copy objects in the list
var list = this.state.list.slice();
// so when changing object, you are directly mutating state
list[itemToChange].highlighted = true;
// setting state will trigger re-render
this.setState({ list: list });
// but the damage is already done:
// e.g. shouldComponentUpdate lifecycle method will fail
// will always return false, even if state did change.
With immutability, you would be doing something quite similar:
// make a copy of the list
var list = this.state.list.slice();
// make a copy of the object to update
var newObject = Object.assign({}, list[itemToChange]);
// update the object copy
newObject.highlighted = true;
// insert the new object into list copy
list[itemToChange] = newObject;
// update state with the new list
this.setState({ list : list );
The above only works if the object does not contain more nested objects.
I am not familiar with immutable.js, but I'm sure they have excellent methods to deal with this more appropriately.
The argument for immutability in react is that you can reliably and transparently work with state changes (also react's life-cycle methods expect them). There are numerous questions on SO with a variant of "why is nextState == this.state", with answers coming down to "not keeping state and props immutable screwed things up"

Categories