Unexpected Results Splicing on Spreaded Array - javascript

This code is for a simple todo app built in react. Today I was trying to refactor my option 1 code into something like option 2 and was surprised to see that it broke my removeTask functionality. During troubleshooting I also tried option 3, which had the same results. I'm struggling to figure out why; to me option 2 and 3 look pretty much the same as option 1, just cleaner. When implementing option 2 or 3 I get no errors, yet clicking removeTask now deletes all the previous tasks. What is the difference between these three?
The problem code:
//Option 1 (working)
removeTask = (event, index) => {
event.stopPropagation();
const removedTaskArray = [...this.state.tasksarray];
removedTaskArray.splice(index, 1);
this.setState({ tasksarray: removedTaskArray });
};
//Option 2 (broken)
removeTask = (event, index) => {
event.stopPropagation();
const removedTaskArray = [...this.state.tasksarray].splice(index, 1);
this.setState({ tasksarray: removedTaskArray });
};
//Option 3 (broken)
removeTask = (event, index) => {
event.stopPropagation();
const copyOfTasksArray = [...this.state.tasksarray]
const removedTaskArray = copyOfTasksArray.splice(index, 1);
this.setState({ tasksarray: removedTaskArray });
};
Full (working) code:
import React, { Component } from 'react';
import './App.css';
/* InputTaskForm renders a form, and returns the input to our storeTask method. */
const InputTaskForm = ({ formValidation }) => {
return (
<form name="charlie" onSubmit={formValidation}>
<input name="userinput" type="text" placeholder="Task..." />
<button type="submit">Submit</button>
</form>
);
}
const DisplayTasks = ({ tasks, removeTask, strikeTask }) => {
return (
<div id="orderedList">
<ol>
{tasks.map((task, index) => (
<li onClick={() => strikeTask(index)} key={index} >
{task.strike ? <strike>{task.title}</strike> : task.title}
<button id="removeButton" onClick={event => removeTask(event, index)} >Remove</button>
</li>
))}
</ol>
</div>
);
};
class App extends Component {
state = {
userinput: '',
tasksarray: [],
}
/* ============================================== #FUNCTIONS ==============================================
=========================================================================================================== */
formValidation = event => { // event prop passed from InputTaskForm component
event.preventDefault(); // prevent form from auto-refreshing on submit
const userInput = event.target.userinput.value // userInput stored
const userInputIsBlank = userInput.trim().length < 1 // trim (remove) prefixed and affixed spaces, then check length
userInputIsBlank
? alert(`Error: invalid submission`)
: this.storeTask(userInput);
};
storeTask = userInput => { // userInput passed from formValidation function
this.setState({
userinput: userInput,
tasksarray: [...this.state.tasksarray, { title: userInput, strike: false } ] //create a copy of tasks array then add a new object into the array filled out with user input
});
document.forms["charlie"].reset();
};
//Option 1 (working)
removeTask = (event, index) => {
event.stopPropagation();
const removedTaskArray = [...this.state.tasksarray];
removedTaskArray.splice(index, 1);
this.setState({ tasksarray: removedTaskArray });
};
strikeTask = index => { // index prop passed from DisplayTasks component
const { tasksarray } = this.state
const selected = tasksarray[index];
this.setState({
tasksarray: [ // change tasksarray state to: [prior slice, change, after slice]
...tasksarray.slice(0, index), // slice off (copies) of array elements prior to index element
Object.assign(selected, {strike: !selected.strike}), // invert the selected line's strike value
...tasksarray.slice(index + 1) // slice off (copies) of array elements after index element
]
});
};
componentDidUpdate() {
console.log(this.state.tasksarray); // debugging :)
};
/* =============================================== #RENDER ================================================
=========================================================================================================== */
render() {
const { tasksarray } = this.state
const { formValidation, storeTask, removeTask, strikeTask } = this
return (
<div>
<InputTaskForm
task={storeTask}
formValidation={formValidation} />
<DisplayTasks
tasks={tasksarray}
removeTask={removeTask}
strikeTask={strikeTask} />
</div>
);
};
};
/* ================================================ #EXPORT ===============================================
=========================================================================================================== */
export default App;

Return value of splice is
An array containing the deleted elements. If only one element is
removed, an array of one element is returned. If no elements are
removed, an empty array is returned.
So in your 2nd and 3rd method you're placing deleted element arrays
so you can simply change your code to
removeTask = (event, index) => {
event.stopPropagation();
const removedTaskArray = [...this.state.tasksarray]
removedTaskArray.splice(index, 1);
this.setState({ tasksarray: removedTaskArray });
};

It all boils down to understanding what does Array##splice it actually returns an array containing the deleted items. Here is a small example:
const a = [1, 2, 3, 4, 5].splice(3, 1); // same as a = [4]
const b = [1, 2, 3, 4, 5];
const c = b.splice(3, 1); // same as b = [1, 2, 3, 5] and c = [4]

Related

Not understanding what's wrong with my Filter function and state in React

I have the following code:
Parent component:
class App extends Component {
state = {
colors: []
};
async componentDidMount() {
const fetched = await fetch("./response.json");
const fetchedJson = await fetched.json();
const res = fetchedJson.colors;
this.setState({
colors: res
});
}
filterItems = (name) => {
const lowerCaseName = name.toLowerCase();
const newColors = [...this.state.colors];
const res = newColors.filter((color) => {
const lowerCaseColorName = color.name.toLowerCase();
return lowerCaseColorName.includes(lowerCaseName);
});
this.setState({
colors: res
});
};
render() {
const { colors } = this.state;
return (
<div>
<InputText filterItems={this.filterItems} />
<AllData colors={colors} />
</div>
);
}
}
And this is my Child component:
class Filter extends Component {
state = {
inputVal: ""
};
onChange = (e) => {
this.setState({
inputVal: e.target.value
});
this.props.filterItems(e.target.value);
};
render() {
return (
<form onSubmit={this.onSubmit}>
<input
type="text"
onChange={this.onChange}
value={this.state.inputVal}
/>
</form>
);
}
}
export default Filter;
There's also another child component called AllData but its job is just displaying out data and put styling on it, so I'm not including it here.
Currently the data displayed are just:
Fire Dragon
Water Horse
Earth Bird
Wood Dog
Wind Cat
Here are my questions:
The filter function works fine when I type in a word into the search box in filter. However, when I backtrack and remove a previous character down to the whole input string, the res array doesn't return its whole original arrays but instead retains the result of the filter only when I type more.
Ex:
When I type in the string "cat", res becomes: [{name: "Wind Cat", id: 5}
However I remove that string by backtracking on the keyboard, res is still at [{name: "Wind Cat", id: 5}. Why is it not going back to returning all of the items, and how do I fix this?
I currently have this code:
onChange = (e) => {
this.setState({
inputVal: e.target.value
});
this.props.filterItems(e.target.value);
};
However, if I change it to:
this.props.filterItems(this.state.inputVal);
and console.log(name) out in the parent component at filterItems, every time I type in a string, it seems like the console.logged name only display the character before.
Ex:
If I type in the string c -> name would be "" (empty)
If I type in the string ca -> name would be c
Why is this happening?
class App extends Component {
state = {
colors: [],
filtered: [],
};
async componentDidMount() {
const fetched = await fetch("./response.json");
const fetchedJson = await fetched.json();
const res = fetchedJson.colors;
this.setState({
colors: res
});
}
filterItems = (name) => {
const lowerCaseName = name.toLowerCase();
const newColors = [...this.state.colors];
const filtered = newColors.filter(color => {
const parts = color.split(' ').map(part => part.toLowerCase());
return parts.reduce((carry, part) => {
return carry ? carry : part.startsWith(lowerCaseName);
}, false);
});
this.setState({
filtered,
});
};
render() {
const { colors, filtered } = this.state;
return (
<div>
<InputText filterItems={this.filterItems} />
<AllData colors={filtered ? filtered : colors} />
</div>
);
}
}
this happens because you filter the array and thus lose the objects. what you can do is have 2 arrays, one with all the data and one with the filtered data. apply the filter to the array with all the data and set the filtered array to the result

How to filter an array and add values to a state

I have the current state as:
const [data, setData] = useState([
{ id: 1, name: "One", isChecked: false },
{ id: 2, name: "Two", isChecked: true },
{ id: 3, name: "Three", isChecked: false }
]);
I map through the state and display the data in a div and call a onClicked function to toggle the isChecked value on click:
const clickData = index => {
const newDatas = [...data];
newDatas[index].isChecked = !newDatas[index].isChecked;
setData(newDatas);
const newSelected = [...selected];
const temp = datas.filter(isChecked==true) // incomplete code, struggling here.
const temp = datas.isChecked ?
};
I have another empty state called clicked:
const[clicked, setClicked] = setState([]). I want to add all the objected whose isChecked is true from the datas array to this array. How can I do this?
I just add checkBox & onChange event instead of using div & onClick event for your understanding
import React, { useState, useEffect } from "react";
import "./style.css";
export default function App() {
const [data, setData] = useState([
{ id: 1, name: "One", isChecked: false },
{ id: 2, name: "Two", isChecked: true },
{ id: 3, name: "Three", isChecked: false }
]);
const [clicked, setClicked] = useState([]);
const clickData = index => {
let tempData = data.map(res => {
if (res.id !== index) {
return res;
}
res.isChecked = !res.isChecked;
return res;
});
setClicked(tempData.filter(res => res.isChecked));
};
useEffect(() => {
setClicked(data.filter(res => res.isChecked));
}, []);
return (
<div>
{data.map((res, i) => (
<div key={i}>
<input
type="checkbox"
checked={res.isChecked}
key={i}
onChange={() => {
clickData(res.id);
}}
/>
<label>{res.name}</label>
</div>
))}
{clicked.map(({ name }, i) => (
<p key={i}>{name}</p>
))}
</div>
);
}
https://stackblitz.com/edit/react-y4fdzm?file=src/App.js
Supposing you're iterating through your data in a similar fashion:
{data.map((obj, index) => <div key={index} onClick={handleClick}>{obj.name}</div>}
You can add a data attribute where you assign the checked value for that element, so something like this:
{data.map((obj, index) => <div key={index} data-checked={obj.isChecked} data-index={index} onClick={handleClick}>{obj.name}</div>}
From this, you can now update your isClicked state when the handleClick function gets called, as such:
const handleClick = (event) => {
event.preventDefault()
const checked = event.target.getAttribute("data-checked")
const index = event.target.getAttribute("data-index")
// everytime one of the elements get clicked, it gets added to isClicked array state if true
If (checked) {
let tempArr = [ ...isClicked ]
tempArr[index] = checked
setClicked(tempArr)
}
}
That will let you add the items to your array one by one whenever they get clicked, but if you want all your truthy values to be added in a single click, then you simply need to write your handleClick as followed:
const handleClick = (event) => {
event.preventDefault()
// filter data objects selecting only the ones with isChecked property on true
setClicked(data.filter(obj => obj.isChecked))
}
My apologies in case the indentation is a bit off as I've been typing from the phone. Hope this helps!

Trying to splice items from my array in ReactJS

I'm trying to splice on a DoubleClick event an item from my list(inventory) but something is not working as I want it. Would be nice if someone could be so kind and help me out to figure out what I'm doing wrong here. For the Info: when I try to slice an item it gets sliced but it is every time the first item and the second item loses all the content inside:
function Inventory() {
const [datas, setData] = useState([
{
id: 1,
label: "Sword",
itemname: "grey_sword",
pieces: 1,
type: "weapon",
},
{
id: 2,
label: "Knife",
itemname: "grey_knife",
pieces: 1,
type: "weapon",
},
]);
useEffect(() => {
const getItems = (data) => {
setData(data);
} // this section is for something else
}, [datas]);
const deleteItem = (index) => {
const test = ([], datas)
test.splice(index, 1)
setData([{test : datas}]);
}
const renderItem= (data, index) => {
return (
<Item
key={data.id}
id={data.id}
type={data.type}
label={data.label}
index={index}
name={data.itemname}
pieces={data.pieces}
deleteItem={deleteItem}
/>
)
}
return (
<div className="inventory-holder">
<div className="inventory-main">
<div className="inventory-information">
<div className="inventory-title">
Inventory
</div>
<div className="inventory-weight-info">
0.20 kg / 1.00 kg
</div>
<div className="inventory-buttons">
<button className="refresh-button" tabIndex="-1"><FontAwesomeIcon icon={faRedo} /></button>
<button className="close-button" tabIndex="-1"><FontAwesomeIcon icon={faTimes} /></button>
</div>
</div>
<div className="items-holder">
<div>{datas.map((data, i) => renderItem(data, i))}</div>
</div>
</div>
</div>
)
}
export default Inventory;
and that would be the Item:
const Item = ({ index, id, type, label, name, pieces, deleteItem }) => {
const useItem = e => {
const type = e.target.getAttribute("itemType");
const index = e.target.getAttribute("data-index");
const pieces = e.target.getAttribute("itempieces");
console.log(type + " " + index + " " + pieces )
if(parseInt(pieces) <= 1){
deleteItem(parseInt(index));
}
}
return(
<div data-index={id} onDoubleClick={useItem} className="inventory-item" itemType={type} itemname={name} itemlabel={label} itempieces={pieces}>
<img className="item-pic" src={chosedtype} ></img>
<div className="item-label">{label}</div>
<div className="item-number-pieces">{pieces}</div>
</div>
);
};
export default Item;
Issue
Array::splice does an in-place mutation.
The splice() method changes the contents of an array by removing or
replacing existing elements and/or adding new elements in place.
The main issue here is state mutation.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comma_Operator
const test = ([], datas) ends up saving the state reference data to test, which is then mutated by test.splice(index, 1), and then strangely enough is overwritten back into state differently setData([{ test: datas }]);.
Solution
A common pattern is to use array::filter instead and filter on the index. filter returns a new array.
const deleteItem = (index) => {
setData(datas => datas.filter((_, i) => i !== index);
}
Your Item is way too complicated:
You can directly use the props without passing them through your div.
const Item = ({ index, id, type, label, name, pieces, deleteItem }) => {
const useItem = () => {
console.log(type + " " + index + " " + pieces )
if(pieces <= 1){
deleteItem(index);
}
}
return(
<div data-index={id} onDoubleClick={useItem} className="inventory-item">
<img className="item-pic" src={chosedtype} ></img>
<div className="item-label">{label}</div>
<div className="item-number-pieces">{pieces}</div>
</div>
);
};
export default Item;
Then your deleteItem function doesn't do what you want:
const deleteItem = (index) => {
const test = ([], datas); //test = datas;
test.splice(index, 1); // test = test without the item at the index
setData([{test : datas}]);//data = [{test: datas}] so an array with an object with the property test = datas (the original array).
}
You should change your deleteItem to something like:
const deleteItem = (index) => {
const newArray = datas.filter((item, i) => i !== index);
setData(newArray);
}

Removing item from an array of items does not work

I am trying to remove an item from a list of items, but it does not seem to work. I have a page where I can add entries dynamically and items can be removed individually too. Adding seems to just work fine.
Sandbox: https://codesandbox.io/s/angry-heyrovsky-r7b4k
Code
import React from "react";
import ReactDOM from "react-dom";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: "",
values: []
};
}
onChange = event => {
this.setState({ value: event.currentTarget.value });
};
onAdd = () => {
this.setState({
value: "",
values: [...this.state.values, this.state.value]
});
};
onRemove = index => {
console.log(index);
let { values } = this.state;
let filteredIDs = values.splice(index, 1);
this.setState({
values: filteredIDs
});
};
render() {
let { values, value } = this.state;
return (
<>
<input
required
placeholder="xyz#example.com"
value={value}
onChange={this.onChange}
/>
<button onClick={this.onAdd}>Add</button>
<div>
<ul className="email-list-holder wd-minus-150">
{values.map((value, index) => (
<li key={index}>
{value}
<button
onClick={() => this.onRemove(index)}
style={{ cursor: "pointer" }}
>
Remove
</button>
</li>
))}
</ul>
</div>
</>
);
}
}
let filteredIDs = values.splice(index, 1); returns the removed item after it removes it from values
you'll want
onRemove = index => {
let { values } = this.state;
values.splice(index, 1);
this.setState({
values
});
tested and works on your codesandbox :p
Here is the working demo for you
https://codesandbox.io/s/purple-snow-kkudc
You have to change the below line.
let filteredIDs = values.splice(index, 1);
Use it instead of above one.
let filteredIDs = values.filter((x, i)=> i!==index);
Hope this will work for you.
I think you are using wrong javascript method when remove the item.
Splice method changes the contents of an array by removing or replacing existing elements and/or adding new elements
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) where begin and end represent the index of items in that array. The original array will not be modified.
Replace
let filteredIDs = values.splice(index, 1);
With
let filteredIDs = values.slice(index, 1);
You are setting the removed part of the array instead of the correct one.
onRemove = index => {
console.log(index);
let { values } = this.state;
values.splice(index, 1);
this.setState({
values
});
};
This should work.
You set the removed items as the new values. This will fix it.
onRemove = index => {
console.log(index);
let { values } = this.state;
let filteredIDs = values.splice(index, 1);
this.setState({
values: values
});
};
splice returns the deleted elements and you are setting the removed elements. You can directly do:
values.splice(index, 1);
this.setState({
values,
})
You can also use uniqueId in order to give each new element a uniqueId this would help in filtering logic.
Here's how I may have structured the state and methods:
this.state = {
values: {
todo1: {
value: 'a'
},
todo2: {
value: 'b'
},
}
}
// Addition
this.setState({
values: {
...this.state.values,
uniqueId: {
value: 'New Value from input'
}
}
});
// Deletion
const stateValues = this.state.values;
delete stateValues[uniqueId];
this.setState({
values: stateValues,
});

How to add an arrow to menu elements that have children?

I am trying to add a FontAwesome arrow next to each item in my menu that has children (i.e. I want to indicate that you can click the element to display more data within that category). The menu is populated with json data from an API, and because it is so many nested objects, I decided to use recursion to make it work. But now I am having trouble adding an arrow only to the elements that have more data within it, instead of every single element in the menu.
Does anyone have an idea of how I could change it so the arrow only shows up next to the elements that need it? See below for image
class Menu extends React.Component {
state = {
devices: [],
objectKey: null,
tempKey: []
};
This is where I'm currently adding the arrow...
createMenuLevels = level => {
const { objectKey } = this.state;
const levelKeys = Object.entries(level).map(([key, value]) => {
return (
<ul key={key}>
<div onClick={() => this.handleDisplayNextLevel(key)}>{key} <FontAwesome name="angle-right"/> </div>
{objectKey[key] && this.createMenuLevels(value)}
</ul>
);
});
return <div>{levelKeys}</div>;
};
handleDisplayNextLevel = key => {
this.setState(prevState => ({
objectKey: {
...prevState.objectKey,
[key]: !this.state.objectKey[key]
}
}));
};
initializeTK = level => {
Object.entries(level).map(([key, value]) => {
const newTemp = this.state.tempKey;
newTemp.push(key);
this.setState({ tempKey: newTemp });
this.initializeTK(value);
});
};
initializeOK = () => {
const { tempKey } = this.state;
let tempObject = {};
tempKey.forEach(tempKey => {
tempObject[tempKey] = true;
});
this.setState({ objectKey: tempObject });
};
componentDidMount() {
axios.get("https://www.ifixit.com/api/2.0/categories").then(response => {
this.setState({ devices: response.data });
});
const { devices } = this.state;
this.initializeTK(devices);
this.initializeOK();
this.setState({ devices });
}
render() {
const { devices } = this.state;
return <div>{this.createMenuLevels(devices)}</div>;
}
}
This is what it looks like as of right now, but I would like it so items like Necktie and Umbrella don't have arrows, since there is no more data within those items to be shown
You could check in the map loop from createMenuLevels if the value is empty or not and construct the div based on that information.
createMenuLevels = level => {
const { objectKey } = this.state;
const levelKeys = Object.entries(level).map(([key, value]) => {
//check here if childs are included:
var arrow = value ? "<FontAwesome name='angle-right'/>" : "";
return (
<ul key={key}>
<div onClick={() => this.handleDisplayNextLevel(key)}>{key} {arrow} </div>
{objectKey[key] && this.createMenuLevels(value)}
</ul>
);
});
return <div>{levelKeys}</div>;
};
Instead of just checking if the value is set you could check if it is an array with: Array.isArray(value)

Categories