I am making a React app that allows you to make a list and save it, but React has been giving me a warning that my elements don't have a unique key prop (elements List/ListForm). How should I create a unique key prop for user created elements? Below is my React code
var TitleForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
var listName = {'name':this.refs.listName.value};
this.props.handleCreate(listName);
this.refs.listName.value = "";
},
render: function() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input className='form-control list-input' type='text' ref='listName' placeholder="List Name"/>
<br/>
<button className="btn btn-primary" type="submit">Create</button>
</form>
</div>
);
}
});
var ListForm = React.createClass({
getInitialState: function() {
return {items:[{'name':'item1'}],itemCount:1};
},
handleSubmit: function(e) {
e.preventDefault();
var list = {'name': this.props.name, 'data':[]};
var items = this.state.items;
for (var i = 1; i < items.length; i++) {
list.data.push(this.refs[items[i].name]);
}
this.props.update(list);
$('#'+this.props.name).remove();
},
handleClick: function() {
this.setState({
items: this.state.items.concat({'name':'item'+this.state.itemCount+1}),
itemCount: this.state.itemCount+1
});
},
handleDelete: function() {
this.setState({
itemCount: this.state.itemCount-1
});
},
render: function() {
var listItems = this.state.items.map(function(item) {
return (
<div>
<input type="text" className="list-form" placeholder="List Item" ref={item.name}/>
<br/>
</div>
);
});
return (
<div>
<form onSubmit={this.handleSubmit} className="well list-form-container">
{listItems}
<br/>
<div onClick={this.handleClick} className="btn btn-primary list-button">Add</div>
<div onClick={this.handleDelete} className="btn btn-primary list-button">Delete</div>
<button type="submit" className="btn btn-primary list-button">Save</button>
</form>
</div>
)
}
});
var List = React.createClass({
getInitialState: function() {
return {lists:[], savedLists: []};
},
handleCreate: function(listName) {
this.setState({
lists: this.state.lists.concat(listName)
});
},
updateSaved: function(list) {
this.setState({
savedLists: this.state.savedLists.concat(list)
});
},
render: function() {
var lst = this;
var lists = this.state.lists.map(function(list) {
return(
<div>
<div key={list.name} id={list.name}>
<h2 key={"header"+list.name}>{list.name}</h2>
<ListForm update={lst.updateSaved} name={list.name}/>
</div>
</div>
)
});
var savedLists = this.state.savedLists.map(function(list) {
var list_data = list.data;
list_data.map(function(data) {
return (
<li>{data}</li>
)
});
return(
<div>
<h2>{list.name}</h2>
<ul>
{list_data}
</ul>
</div>
)
});
var save_msg;
if(savedLists.length == 0){
save_msg = 'No Saved Lists';
}else{
save_msg = 'Saved Lists';
}
return (
<div>
<TitleForm handleCreate={this.handleCreate} />
{lists}
<h2>{save_msg}</h2>
{savedLists}
</div>
)
}
});
ReactDOM.render(<List/>,document.getElementById('app'));
My HTML:
<div class="container">
<h1>Title</h1>
<div id="app" class="center"></div>
</div>
There are many ways in which you can create unique keys, the simplest method is to use the index when iterating arrays.
Example
var lists = this.state.lists.map(function(list, index) {
return(
<div key={index}>
<div key={list.name} id={list.name}>
<h2 key={"header"+list.name}>{list.name}</h2>
<ListForm update={lst.updateSaved} name={list.name}/>
</div>
</div>
)
});
Wherever you're lopping over data, here this.state.lists.map, you can pass second parameter function(list, index) to the callback as well and that will be its index value and it will be unique for all the items in the array.
And then you can use it like
<div key={index}>
You can do the same here as well
var savedLists = this.state.savedLists.map(function(list, index) {
var list_data = list.data;
list_data.map(function(data, index) {
return (
<li key={index}>{data}</li>
)
});
return(
<div key={index}>
<h2>{list.name}</h2>
<ul>
{list_data}
</ul>
</div>
)
});
Edit
However, As pointed by the user Martin Dawson in the comment below, This is not always ideal.
So whats the solution then?
Many
You can create a function to generate unique keys/ids/numbers/strings and use that
You can make use of existing npm packages like uuid, uniqid, etc
You can also generate random number like new Date().getTime(); and prefix it with something from the item you're iterating to guarantee its uniqueness
Lastly, I recommend using the unique ID you get from the database, If you get it.
Example:
const generateKey = (pre) => {
return `${ pre }_${ new Date().getTime() }`;
}
const savedLists = this.state.savedLists.map( list => {
const list_data = list.data.map( data => <li key={ generateKey(data) }>{ data }</li> );
return(
<div key={ generateKey(list.name) }>
<h2>{ list.name }</h2>
<ul>
{ list_data }
</ul>
</div>
)
});
It is important to remember that React expects STABLE keys, meaning you should assign the keys once and every item on your list should receive the same key every time, that way React can optimize around your data changes when it is reconciling the virtual DOM and decides which components need to re-render.
So, if you are using UUID you need to do it at the data level, not at the UI level.
Also keep in mind you can use any string you want for the key, so you can often combine several fields into one unique ID, something like ${username}_${timestamp} can be a fine unique key for a line in a chat, for example.
Keys helps React identify which items have changed/added/removed and should be given to the elements inside the array to give the elements a stable identity.
With that in mind, there are basically three different strategies as described bellow:
Static Elements (when you don't need to keep html state (focus, cursor position, etc)
Editable and sortable elements
Editable but not sortable elements
As React Documentation explains, we need to give stable identity to the elements and because of that, carefully choose the strategy that best suits your needs:
STATIC ELEMENTS
As we can see also in React Documentation, is not recommended the use of index for keys "if the order of items may change. This can negatively impact performance and may cause issues with component state".
In case of static elements like tables, lists, etc, I recommend using a tool called shortid.
1) Install the package using NPM/YARN:
npm install shortid --save
2) Import in the class file you want to use it:
import shortid from 'shortid';
2) The command to generate a new id is shortid.generate().
3) Example:
renderDropdownItems = (): React.ReactNode => {
const { data, isDisabled } = this.props;
const { selectedValue } = this.state;
const dropdownItems: Array<React.ReactNode> = [];
if (data) {
data.forEach(item => {
dropdownItems.push(
<option value={item.value} key={shortid.generate()}>
{item.text}
</option>
);
});
}
return (
<select
value={selectedValue}
onChange={this.onSelectedItemChanged}
disabled={isDisabled}
>
{dropdownItems}
</select>
);
};
IMPORTANT: As React Virtual DOM relies on the key, with shortid every time the element is re-rendered a new key will be created and the element will loose it's html state like focus or cursor position. Consider this when deciding how the key will be generated as the strategy above can be useful only when you are building elements that won't have their values changed like lists or read only fields.
EDITABLE (sortable) FIELDS
If the element is sortable and you have a unique ID of the item, combine it with some extra string (in case you need to have the same information twice in a page). This is the most recommended scenario.
Example:
renderDropdownItems = (): React.ReactNode => {
const elementKey:string = 'ddownitem_';
const { data, isDisabled } = this.props;
const { selectedValue } = this.state;
const dropdownItems: Array<React.ReactNode> = [];
if (data) {
data.forEach(item => {
dropdownItems.push(
<option value={item.value} key={${elementKey}${item.id}}>
{item.text}
</option>
);
});
}
return (
<select
value={selectedValue}
onChange={this.onSelectedItemChanged}
disabled={isDisabled}
>
{dropdownItems}
</select>
);
};
EDITABLE (non sortable) FIELDS (e.g. INPUT ELEMENTS)
As a last resort, for editable (but non sortable) fields like input, you can use some the index with some starting text as element key cannot be duplicated.
Example:
renderDropdownItems = (): React.ReactNode => {
const elementKey:string = 'ddownitem_';
const { data, isDisabled } = this.props;
const { selectedValue } = this.state;
const dropdownItems: Array<React.ReactNode> = [];
if (data) {
data.forEach((item:any index:number) => {
dropdownItems.push(
<option value={item.value} key={${elementKey}${index}}>
{item.text}
</option>
);
});
}
return (
<select
value={selectedValue}
onChange={this.onSelectedItemChanged}
disabled={isDisabled}
>
{dropdownItems}
</select>
);
};
Hope this helps.
Do not use this return `${ pre }_${ new Date().getTime()}`;. It's better to have the array index instead of that because, even though it's not ideal, that way you will at least get some consistency among the list components, with the new Date function you will get constant inconsistency. That means every new iteration of the function will lead to a new truly unique key.
The unique key doesn't mean that it needs to be globally unique, it means that it needs to be unique in the context of the component, so it doesn't run useless re-renders all the time. You won't feel the problem associated with new Date initially, but you will feel it, for example, if you need to get back to the already rendered list and React starts getting all confused because it doesn't know which component changed and which didn't, resulting in memory leaks, because, you guessed it, according to your Date key, every component changed.
Now to my answer. Let's say you are rendering a list of YouTube videos. Use the video id (arqTu9Ay4Ig) as a unique ID. That way, if that ID doesn't change, the component will stay the same, but if it does, React will recognize that it's a new Video and change it accordingly.
It doesn't have to be that strict, the little more relaxed variant is to use the title, like Erez Hochman already pointed out, or a combination of the attributes of the component (title plus category), so you can tell React to check if they have changed or not.
edited some unimportant stuff
Let React Assign Keys To Children
You may leverage React.Children API:
const { Children } = React;
const DATA = [
'foo',
'bar',
'baz',
];
const MyComponent = () => (
<div>
{Children.toArray(DATA.map(data => <p>{data}</p>))}
</div>
);
ReactDOM.render(<MyComponent />,document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
To add the latest solution for 2021...
I found that the project nanoid provides unique string ids that can be used as key while also being fast and very small.
After installing using npm install nanoid, use as follows:
import { nanoid } from 'nanoid';
// Have the id associated with the data.
const todos = [{id: nanoid(), text: 'first todo'}];
// Then later, it can be rendered using a stable id as the key.
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
)
Another option is weak-key: https://www.npmjs.com/package/weak-key
import weakKey from "weak-key";
const obj1 = {a : 42};
const obj2 = {b : 123};
const obj3 = {a : 42};
console.log(weakKey(obj1)); // 'weak-key-1'
console.log(weakKey(obj2)); // 'weak-key-2'
console.log(weakKey(obj3)); // 'weak-key-3'
console.log(weakKey(obj1)); // 'weak-key-1'
For a simple array of text-strings; I'm trying one of the two ways:
1. encodeURI which is available on both; NodeJS and browser
const WithEncoder = () => {
const getKey = useCallback((str, idx) => encodeURI(`${str},${idx}`), [])
return (
<div>
{["foo", "bar"].map((str, idx) => (
<div key={getKey(str, idx)}>{str}</div>
))}
</div>
)
}
2. window.btoa which is available only in browser.
const WithB2A = () => {
const getKey = useCallback((str, idx) => window.btoa(`${str}-${idx}`), [])
return (
<div>
{["foo", "bar"].map((str, idx) => (
<div key={getKey(str, idx)}>{str}</div>
))}
</div>
)
}
Depends on the situation, choose a uniqueId creator is ok when you just want render silly items, but if you render items like drag&drop etc and you haven't any uniqueId for each item, I recommend remap that data in your redux, mapper, wherever and add for each item an uniqueId (and not in the render like <Item key={...}) because React couldn't perform any check between renders (and with that all the benefits).
With that remapped that you can use that new Id in your Component.
Here is what I have done, it works for reordering, adding, editing and deleting. Once set the key is not changed, so no unnecessary re-render. One PROBLEM which may be a show stopper for some: it requires adding a property to your object at first render say "_reactKey".
Example for functional component in psuedo TS (ie it won't run in snippets):
interface IRow{
myData: string,
_reactKey?:number
}
export default function List(props: {
rows: Array<IRow>
}) {
const {myRows} = props;
const [nextKey, setNextKey] = useState(100);
const [rows, setRows] = useState<Array<IRow>|undefined>();
useEffect(function () {
if (myRows) {
for (let row of myRows){
if (!row._reactKey){
row._reactKey = nextKey;
setNextKey(nextKey+1);
}
}
setRows(myRows);
} else if (!rows) {
setRows([]);
}
}, [myRows, columns]);
addRow(){
let newRow = { blah, blah, _reactKey : nextKey};
setNextKey(nextKey+1);
rows.push(newRow);
setRows({...rows});
}
function MyRow(props:{row:IRow}){
const {row} = props;
return <tr><td>{row._reactKey}</td><td>row.myData</td></tr>
}
return <table>
<tr><th>Index</th><th>React Key</th><th>My Data</th></tr>
rows.map((row, key)=>{
return <MyRow key={row._reactKey} row={row} />
}
</table>
}
I don't use react too much, but the last time I saw this issue I just created a new state array, and tracked the keys there.
const [keys, setKeys] = useState([0]);
const [items, setItems] = useState([value: "", key: 0,])
Then when I add a new item to list, I get the last key from the keys array, add 1, then use setKeys to update the keys array. Something like this:
const addItemWithKey = () => {
// create a new array from the state variable
let newKeyArr = [...keys];
// create a new array from the state variable that needs to be tracked with keys
let newItemArr = [...items];
// get the last key value and add 1
let key = newKeyArr[newKeyArr.length-1] + 1;
newKeyArr.push(key);
newItemArr.push({value: "", key: key,});
// set the state variable
setKeys(newKeyArr);
setItems(newItemArr);
};
I don't worry about removing values from the keys array because it's only being used for iterating in the component, and we're trying to solve for the case where we remove an item from the list and/or add a new item. By getting the last number from the keys array and adding one, we should always have unique keys.
import React, {useState} from 'react';
import {SafeAreaView,ScrollView,StyleSheet,Text,View,Dimensions} from 'react-native';
const {width}=Dimensions.get('window');
function sayfalar(){
let pages=[]
for (let i = 0; i < 100; i++) {
pages.push(<View key={i} style={styles.pages}><Text>{i}</Text></View>)
}
return pages
}
const App=()=>{
return(
<View style={styles.container}>
<ScrollView horizontal={true} pagingEnabled={true}>
{sayfalar()}
</ScrollView>
</View>
)
}
const styles = StyleSheet.create({
container:{
flexDirection:'row',
flex:1
},
pages:{
width:width
}
})
export default App;
Use the mapped index (i)
things.map((x,i) => {
<div key=i></div>
});
Hope this helps.
You can use react-html-id to generate uniq id easely : https://www.npmjs.com/package/react-html-id
The fastest solution in 2021 is to use uniqid: Go to https://www.npmjs.com/package/uniqid for more info but to sum up:
First in your terminal and your project file: npm install uniqid
Import uniqid in your project
Use it in any key that you need!
uniqid = require('uniqid');
return(
<div>
<div key={ uniqid() } id={list.name}>
<h2 key={ uniqid() }>{list.name}</h2>
<ListForm update={lst.updateSaved} name={list.name}/>
</div>
</div>
)
});
I am using this:
<div key={+new Date() + Math.random()}>
when I click on the button to sort the data about countries after the website is loaded then the data displays correclty. But when I want to sort it again by different value, the data doesn't update and the state also doesn't update, however, the function works well, as I can see that the results in the console are sorted correctly. What can be the reason for this weird behaviour? Thank you for your time.
function AllCountries({ countries }) {
const [sortedCountries, setSortedCountries] = useState([]);
useEffect(() => {
console.log(sortedCountries)
}, [sortedCountries])
const sortResults = (val) => {
let sortedResults = countries.sort((a, b) => {
if (val === "deaths") {
return b.TotalDeaths - a.TotalDeaths;
} else if (val === "cases") {
return b.TotalConfirmed - a.TotalConfirmed;
} else if (val === "recovered") {
return b.TotalRecovered - a.TotalRecovered;
}
});
console.log(sortedResults);
setSortedCountries(sortedResults);
};
return (
<div className="all-container">
<p>Sort countries by the highest number of:</p>
<button onClick={() => sortResults("deaths")}>Deaths</button>
<button onClick={() => sortResults("cases")}>Cases</button>
<button onClick={() => sortResults("recovered")}>Recoveries</button>
<ul className="all-countries">
{sortedCountries.map((country, key) => {
return <li key={key}>{country.Country}</li>;
})}
</ul>
</div>
);
}
export default AllCountries;
Array.sort() method doesn't return new reference, it returns the same reference. So I assume in sortedCountries state is stored the same props.countries reference, that is why second button click doesn't set the state (it is the same reference), I can give you simple solution just change this line setSortedCountries(sortedResults) with this setSortedCountries([...sortedResults]) in this case copy of the sortedResults will be passed to setSortedCountries (new reference).
let sortedResults = countries.sort
this line is sorting the countries props and setting sortedResults to that pointer
Let us assume that pointer is held at mem |0001| for simplicity
After your first click, the function is fired, the prop is sorted, and sortedResults is set via setSortedCountries (set state).
This fires off the render that you desire, because the previous state was undefined, and now the state is pointing to |0001|
When your function runs again, the sort function fires off, does its work on |0001| and returns -- you guessed it -- |0001| but with your new sorted array.
When you go to set the state a second time, there wasn't actually any state changed because the previous country is |0001| and the country you want to change to is |0001|
So what can we do my good sir?
First I need you to think about the problem for a second, think about how you could solve this and try to apply some changes.
What you can try to do is copy the countries props to a new array pointer with the same values, and then sort it, and then set sortedCountries state to that list.
Does that make sense?
By the way, this is for similar reasons why if you try to setState directly on an new object state, the new object state will be exactly equal to the one you set. There isn't any real magic going on here. React does not automagically merge your previous object state and your new one, unless you tell it explicitly to do so.
In some sense you have told react to check differences between two states, an old country and a new country state (behind the scenes with the Vdom), but to react, those two states have no difference. And since the two object states have no difference, there will be no actual DOM changes.
You setting a pointer twice will therefore produce no actual changes.
You must therefore set state to an actual new pointer with new values, so the react virtual dom can compare the two states (the previous countries list) and the new countries list.
Hope that helps
-
It's a bit problem in React and another solution is by using the useReducer instead. such as below:
function reducer(state, action) {
return [...state, ...action];
}
function AllCountries({ countries }) {
const [sortedCountries, setSortedCountries] = useReducer(reducer, []);
useEffect(() => {
console.log(sortedCountries)
}, [sortedCountries])
const sortResults = (e, val) => {
e.preventDefault();
let sortedResults = countries.sort((a, b) => {
if (val === "deaths") {
return b.TotalDeaths - a.TotalDeaths;
} else if (val === "cases") {
return b.TotalConfirmed - a.TotalConfirmed;
} else if (val === "recovered") {
return b.TotalRecovered - a.TotalRecovered;
}
});
console.log(sortedResults);
setSortedCountries(sortedResults);
};
return (
<div className="all-container">
<p>Sort countries by the highest number of:</p>
<button onClick={(e) => sortResults(e, "deaths")}>Deaths</button>
<button onClick={(e) => sortResults(e, "cases")}>Cases</button>
<button onClick={(e) => sortResults(e, "recovered")}>Recoveries</button>
<ul className="all-countries">
{sortedCountries.map((country, key) => {
return <li key={key}>{country.Country}</li>;
})}
</ul>
</div>
);
}
export default AllCountries;
I'm learning React by implementing a front-end interface for the note app API that I created. I have succeeded in having a list of all the note titles in my database appear. I want to be able to click on a title and have the note expand into the text of the note. The easiest way I've found for this is to give the "key" attribute of the 'li' as a variable and to also declare the same variable in the JSX { } object because they have the same name.
I've been looking for an answer for this for a few days and have been unable to find this exact problem. You can put a variable in a normal JSX expression but I need to do it on the 'li' which means technically in the HTML.
Here's some code to understand what I'm saying.
const NoteData = () => {
const [titles, setTitles] = useState([]);
const [open, setOpen] = useState(false);
useEffect(() => {
//AXIOS CALL
setTitles(response.data[0]);
});
}, []);
//^^^^^add the array there to stop the response.data from repeating WAY TOO MANY TIMES
let listTitles = titles.map(titles => (
<li className="noteTitles" key={titles.title}>
{titles.title}
</li>
));
let showText = titles.map(titles => (
<li className="openText" key= {titles.text_entry}>
{titles.text_entry}
</li>
))
let openNote = () => {
setOpen(open => !open);
if (open) {
return (
<div className="noteContainer">
<ul onClick={openNote} className="titlesList">
{showText}
</ul>
</div>
);
}
if (!open) {
return (
<div className="noteContainer">
<ul onClick={openNote} className="titlesList">
{listTitles}
</ul>
</div>
);
}
};
return { openNote };
};
export default NoteData;
That is the code I currently have. Here's showing a more simplified version of the openNote function that maybe makes more sense and shows what I'm trying to do:
VariableHere = "";
let openNote = () => {
setOpen(open => !open);
open ? (VariableHere = titles.text_entry) : (VariableHere = titles.title);
};
let listNotes = titles.map(titles => (
<li className="noteTitles" key={VariableHere}>
{VariableHere}
</li>
));
return (
<div>
<ul onClick={openNote}>
{listNotes}
</ul>
</div>
);
On click of each element there should be a switch of the key elements so if the element is 'open' the key variable and given variable in the JSX object should be mapped to titles.text_entry and on '(!open)' the key and JSX should be mapped to titles.title.
first of all, you're using a ternary in a weird way:
open ? (VariableHere = titles.text_entry) : (VariableHere = titles.title);
Ternaries are meant to be expressions whose value is conditional, but you're using it like a shorthand if/else. Try something like
VariableHere = open ? titles.text_entry : titles.title;
which is both shorter and more readable.
Second of all, keys in an array of elements are meant to help React determine which elements to update, if an item represents the same object, its key shouldn't change. In this case, regardless of what you're displaying, an item in the array represents the same note. Always using the title as the key should be fine provided items can't have the same title. If they can, use some sort of unique ID instead. If the order of the items doesn't change throughout the life of the component, using the array index as the key is fine.
Lastly, what you seem to want to do is called "conditional rendering". There are many ways to achieve this in react, one such way is to use the pre-cited ternary operator. Here is a minimal working example:
const listNotes = titles.map(note => (
<li className="noteTitles" key={note.title}>
{open ? note.title : note.text_entry}
</li>
));
const openNote = () => {
setOpen(!open);
}
return (
<div className="noteContainer">
<ul onClick={openNote} className="titlesList">
{listNotes}
</ul>
</div>
)
You could also use a ternary in the key expression, but as I talked about above, it's not a good idea to do so.
Given your data-structure, I think you can simplify your code a bit. There is no need to create separate arrays for titles and contents. It sounds like you just want to expand and collapse a note when it is selected.
Here is a really simplified version on how you an do this. I'll use a sample data-set since we don't have access to your API.
const NoteData = () => {
const [titles, setTitles] = useState([]);
const [currentNote, setCurrentNote] = useState({});
useEffect(() => {
//AXIOS CALL
// setTitles(response.data[0]);
let data = [
{ id: 1, title: "a", text_entry: "what" },
{ id: 2, title: "b", text_entry: "is" },
{ id: 3, title: "c", text_entry: "up?" }
];
setTitles(data);
}, []);
const handleClick = noteId => {
let selectedTitle = titles.find(title => title.id == noteId);
//"collapse" if already selected
if (noteId === currentNote.id) {
setCurrentNote({});
} else {
setCurrentNote(selectedTitle);
}
};
let listTitles = titles.map(title => (
<li
className="noteTitles"
key={title.title}
onClick={() => handleClick(title.id)}
>
{title.title}
{title.id === currentNote.id && <div>{title.text_entry}</div>}
</li>
));
return (
<div>
Click on link item
<ul>{listTitles}</ul>
</div>
);
};
See working sandbox: https://codesandbox.io/s/old-silence-366ne
The main updates:
You don't need to have an "open" state. To be more succinct and
accurate, you should have a currentNote state instead, which is
set when clicking on a list item.
Have your handleClick function accept a noteId as an argument.
Then use that noteId to find the corresponding note in your titles
state. Set that found note as the currentNote. If the selected
note was already the currentNote, simply set currentNote to an
empty object {}, thus creating our expanding/collapsing effect.
In the JSX, after the title, use a ternary operator to conditionally
display the currentNote. If the note being mapped matches the
currentNote, then you would display a div containing the
text_entry.
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
});
};
I am trying to remove a value from my state.
I am using .filter as I believe this is the simplest way of doing it. I also want to implement an undo function ( but that's outside the scope of this question).
I have put this code in a sandbox
https://codesandbox.io/s/yrwo2PZ2R
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
movies: x.movies,
};
}
remove = (e) => {
e.preventDefault();
console.log('remove movie.id:', e.target.value)
const index = e.target.value
this.setState({
movies: this.state.movies.filter((_, e) => e.id !== index)
});
}
render() {
return (
<div>
{this.state.movies.map(e =>
<div key={e.id}>
<li>{e.name} {e.id}</li>
<button value={e.id} onClick={this.remove}>remove</button>
</div>,
)}
</div>
);
}
}
Two problems.
First of all, the index you're getting from the event target value is a string, but you're comparing against a number. Change the index declaration as follows:
const index = Number(e.target.value);
Secondly, your filter is a little off. This will work:
this.state.movies.filter(movie => movie.id !== index)
The problem is index has string type, but id in objects has number type. You need type cast, for example:
const index = Number(e.target.value);
Other than that, you have some wrong _ in callback of filter function call. You don't need it. You need:
this.state.movies.filter(e => e.id !== index)
By the way I don't recommend to name values this way. Why e? You have array of movies. Use movie. Why index? You have id to remove. Then use idToRemove name.
You also have problem with adding items.
Firstly, you can add items like this:
this.setState({
movies: [...this.state.movies, { name: item.value.name, id: item.value.id }],
})
Another point: you have to autoincrement id. You can store last value in a variable. this.idCounter for example. And add will look like:
this.setState({
movies: [...this.state.movies, { name: item.value.name, id: this.idCounter++ }],
})
Example: https://codesandbox.io/s/2vMJQ3p5M
You can achieve the same in the following manner
remove = (e) => {
e.preventDefault();
console.log('remove movie.id:', e.target.value)
const index = e.target.value
var movies = [...this.state.movies]
var idx = movies.findIndex((obj) => obj.id === parseInt(index))
movies.splice(idx, 1);
this.setState({
movies
});
}
Also use parseInt to convert index to a string before comparing.
Directly setting the current state from the previous state values can cause problems as setState is asynchronous. You should ideally create a copy of the object and delete the object using splice method
CODESANDBOX