I'm working on a React component that the user can dynamically add / remove children (kind of like a todo-list style).
The challenge is that the user can click a button to add in / remove children and, so, any new child may not have anything unique about it to set as the key element (aside from, maybe, the creation time) and, since, the user can delete any one of these component and, then, re-add more, the index won't work.
Here's what I've come up with that seems to work:
class ParentComponent extends Component {
constructor(props) {
super(props);
this.state = {
data: [ XXXX ]
};
}
addSection(title, content, key = this.generateNewKeyVal()) {
this.setState({
data:
[...this.state.data,
{
title: title,
content: content,
key: key
}
]
});
}
generateNewKeyVal() {
if(this.state.data.length === 0)
return 1;
return Math.max.apply(Math, this.state.data.map(d => d.key)) + 1;
}
removeFunc(key) {
this.setState({
data: this.state.data.filter(e1 => e1.key !== key)
});
}
}
export default ParentComponent;
As I said, my generateNewKeyVal() function seems to work perfectly since it ensures it generates a new, unique integer value for key (based upon the values currently in the array, that is) and, so long as that key remains in the array, the function will ensure a higher number will be created for a new item's key.
My challenge is that I'm SO new to React that I'd like to make sure I'm not making some huge mistake here or if there is a better way to generate a key in this kind of situation.
Related
I currently dynamically render the same component when clicking a button and the latest component is rendered on the top of the list.
Now, I want to delete the component. Each component has a cancel button to delete the rendered component. So I should be able to delete the component no matter where it is in the list.
Here's what I have so far:
local state:
state = {
formCount: 0
}
add and cancel:
onAddClicked = () => {
this.setState({formCount: this.state.formCount + 1});
}
onCancelButtonClicked = (cancelledFormKey: number) => {
const index = [...Array(this.state.formCount).keys()].indexOf(cancelledFormKey);
if (index > -1) {
const array = [...Array(this.state.formCount).keys()].splice(index, 1);
}
}
Parent component snippet:
{ [...Array(this.state.formCount).keys()].reverse().map(( i) =>
<Form key={i}
onCancelButtonClicked={() => this.onCancelButtonClicked(i)}
/>)
}
The only thing is I'm not sure how to keep track of which form was cancelled/deleted. I think I would need to create a new object in my local state to keep track but how do I know which index of the array I deleted as part of state? I'm not sure how do that? As I am using the count to make an array above.
Usually, this isn't how you'd generate a list of items. You're not storing the form data in the parent, and you're using index based keys which is a no-no when you're modifying the array. For example, I have an array of size 5 [0, 1, 2, 3, 4], when I remove something at position 2, the index of all the items after it changes causing their key to change as well, which will make react re-render them. Since you're not storying the data in the parent component, you will lose them.
Just to humor you, if we want to go with indexed based keys, we may have to maintain a list of removed indexes and filter them out. Something like this should do the trick:
state = {
formCount: 0,
deletedIndex: []
}
onCancelButtonClick = (cancelledIndex: number) => setState((prevState) => ({
deletedIndex: [...prevState.deletedIndex, cancelledIndex]
});
And your render would look like:
{
[...Array(this.state.formCount)].keys()].reverse().map((i) => (
if (deletedIndex.includes(i) {
return null;
} else {
<Form key={i} ... />
}
))
}
As a rule of thumb though, avoid having index based keys even if you don't care about performance. It'll lead to a lot of inconsistent behavior, and may also cause the UI and the state to be inconsistent. And if you absolutely want to for fun, make sure the components that are being rendered using index based keys have their data stored at the parent component level
this.state = {
myArray = [
{
name:"cat",
expand:false
}
]
}
clickItem(item){
item.expand = true;
this.setState({})
}
this.state.myArray.map((item) =>{
return <div onClick={()=>this.clickItem(item)}>{item.name}</div>
})
In React, i have a simple array of objects,
when i click on one of theses object, i want to change their prop and update the state, what is the proper way of doing this.
i feel like there could be a better way
You need to copy your state, update the copied state and the set the state.
this.state = {
myArray = [
{
name:"cat",
expand:false
}
]
}
clickItem(key){
let items = this.state.myArray;
items[key].expand = true;
this.setState({items})
}
this.state.myArray.map((key, item) =>{
return <div onClick={()=>this.clickItem(key)}>{item.name}</div>
})
Okay, a couple of things.
You're mutating the state directly which is going to fail silently and you're also missing the key prop on your <div.
This is easily resolved though by using the data you have available to you. I don't know whether each name is unique but you can use that as your key. This helps React decide which DOM elements to actually update when state changes.
To update your item in state, you need a way to find it within the state originally, so if name is unique, you can use Array.prototype.find to update it.
clickItem(item) {
const targetIndex = this.state.items.find(stateItem => stateItem.name === item.name)
if (targetIndex === -1)
// Handle not finding the element
const target = this.state.items[targetIndex]
target.expand = !target.expand // Toggle instead of setting so double clicking works as expected.
this.setState({
items: this.state.items.splice(targetIndex, 1, target) // This replaces 1 item in the target array with the new one.
})
}
This will update state and re-render your app. The code is untested but it should work.
I am trying to dynamically create/remove a Vue component. I have figured out how to dynamically add the component, but I am having some troubles with allowing the users to remove the specific component.
Consider below two Vue files:
TableControls.vue
<a v-on:click="addColumn">Add Column</a>
<script>
export default {
methods: {
addColumn: function () {
Event.$emit('column-was-added')
}
}
};
</script>
DocumentViewer.vue:
<div v-for="count in columns">
<VueDragResize :id="count">
<a #click="removeColumn(count)">Remove Column</a>
</VueDragResize>
</div>
<script>
import VueDragResize from 'vue-drag-resize';
export default {
components: {
VueDragResize
},
data() {
return {
columns: [1],
}
},
created() {
Event.$on("column-was-added", () => this.addColumn())
},
methods: {
addColumn: function () {
this.columns.push(this.columns.length + 1)
},
removeColumn: function (id) {
this.columns.splice(id, 1)
}
}
};
</script>
As you can see, whenever a user clicks on <a v-on:click="addColumn">Add Column</a>, it will submit an event, and the DocumentViewer.vue file will pick up it, firing the addColumn method. This will ultimately create a new <VueDragResize></VueDragResize> component.
This works great.
The problem is when I want to remove the component again. My removeColumn method simply removes an id from the columns array:
removeColumn: function (id) {
this.columns.splice(id, 1)
}
This results in that a column is in fact removed. However, consider below example. When user clicks on the remove icon for the first column, it will remove the 2nd column instead. (And when there is only one column present, it cannot be removed).
I believe this is due to the fact that I splice() the array, but I cannot see how else I can remove the component dynamically?
I see, Array on Vue does not re render when you modify them.
You need to use the
Vue.set(items, indexOfItem, newValue)
if you want to modify
and use
Vue.delete(target, indexOfObjectToDelete);
If you want to delete an item from an array
You may read the additional info here
https://v2.vuejs.org/v2/api/#Vue-delete
If you want to delete an item from array. Using this will cause the component to rerender.
In this case it will be intuitive to do this
removeColumn: function (id) {
Vue.delete(this.columns, id)
}
Note that id should be the index. Vue.delete ensures the re-render of the component.
EDIT, you must use the index, instead of the count here.
<div v-for="(count, index) in columns">
<VueDragResize :id="index">
<a #click="removeColumn(index)">Remove Column</a>
</VueDragResize>
</div>
I would recommend reshaping your data, each element should be an object with an id and whatever other properties you want. Not simply an id then you would need something like this:
removeColumn(id) {
const elToRemove = this.columns.findIndex(el => el.id === id)
let newArr = [elToRemove, ...this.columns]
this.columns = newArr
}
Also make another computed property for columns like this to make sure they change dynamically (when you add/remove):
computed: {
dynColumns(){ return this.columns}
}
I have same problem, and I found the solution of this problem. It is need to set #key with v-for. This is Built-in Special Attributes.
By default, if you do not set "#key", array index is set to#key. So if array length is 3, #key is 0,1,2. Vue identify eash v-for elements by key. If you remove second value of array, then array index is 0 and 1, because array length is 2. Then Vue understand that #key==2 element removed, So Vue remove 3rd component. So if you remove second value of array, if no #key, third component will be removed.
To avoid this, need to set #key to identify component like this:
let arr = [
{ id: 'a', ...},
{ id: 'b', ...},
{ id: 'c', ...}
];
<div v-for="obj in arr" :key="obj.id">
<someYourComponent>
...
</someYourComponent>
</div>
So I've been given vague instruction on a school project:
.map an icon that when clicked, runs a function that puts the key of
the .map in as the argument which then grabs the image and audio
I haven't been given any base code to work with. I'm just not sure where to start. Any help would be appreciated on using .map
This is really vague. Are you sure your professor didn't have any additional instructions? Does your professor want this as an element on a webpage or a standalone icon/app/exe that runs on the desktop?
So map is a method on the Array class. It creates a new array from the results of performing a function (callback) on every element of the initial array. (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)
//example
const arrayOfNumbers = [1, 2, 3, 4]
// returns array of each element multiplied by 2
arrayOfNumbers.map(num => num*2)
// = [2, 4, 6, 8]
It sounds like you have some sort of mapping (object/hash) that has a key and points to the image/audio source (url). It looks like you would want some image/icon that when click, fires off an event hander whose callback would be to take the keys from this object and key into the object using map to grab your files sources. Then you can display it or something on the DOM?
But it definitely sounds like you should've received additional information to solve this problem.
Here's an actual example that you can dig around.
We start off by creating a library of records. This library is just an array of items (in the form of objects) that we want to access. Each object within the array has an id (that we will use to access records and store an active record id), title, image url and a sound file url.
We create a parent component that contains simple logic to set an active record id. We don't actually use this function within the parent components render method but pass it down to our <Record/> component to use.
We render the libary in the method but we just map the library id's into a new array before mapping the <Record/> component because your assignment criteria is to return an image and a sound by mapping an id.
We create a simple component that renders a record and uses the set prop that we have passed down from the parent component to set the active record id. Since we need to locate the record first we use a find to filter down the Library array, so that we have a single object that we can use to display the title.
The handle click method was created to run the set prop function as we don't want to just do onClick={() => this.props.set(this.props.id)} in the render method because it'll create a new function every time the component is re-rendered. (This is an optimisation)
Lastly but not least, the <ActiveRecord/> presentation component is similar to the above but this finds a record based on the active record id that has been passed down. This component renders the image and sound.
Have a play around! This code could be shortened but we really want to hit the assessment criteria where we need to map id's which makes the <Record/> component a little more complicated (by not passing down the whole record down and having to use .find)
Let me know if you need more information.
Notes: We are not using class transform properties.
/**
* Static const that keeps a list of records
*/
const Library = [
{
id: 1,
title: 'Service Bell',
sound: 'http://soundbible.com/grab.php?id=2218&type=mp3',
image: 'https://i.ebayimg.com/images/i/401039903298-0-1/s-l1000.jpg',
},
{
id: 2,
title: 'Dog',
sound: 'http://soundbible.com/grab.php?id=2215&type=mp3',
image: 'https://lovinlife.com/wp-content/uploads/2018/09/Dog.jpg',
}
]
class App extends React.Component {
constructor(props) {
super(props);
/**
* State for storing selected record id
*/
this.state = {
activeRecordId: null,
};
this.setActiveRecord = this.setActiveRecord.bind(this);
};
/**
* Sets active record
* #param id {string}
*/
setActiveRecord(id) {
this.setState({
activeRecordId: id,
});
};
/**
* Render
*/
render() {
return (
<div className="App">
<h5>Library</h5>
{Library.map(record => record.id).map(recordId => <Record recordId={recordId} set={this.setActiveRecord} />)}
{this.state.activeRecordId && <ActiveRecord activeRecordId={this.state.activeRecordId}/>}
</div>
)
}
}
/**
* Displays a record
*/
class Record extends React.Component {
constructor(props) {
super(props);
// binds handle click so that you can access
// props within the function
this.handleClick = this.handleClick.bind(this);
};
/**
* Call parent prop that sets the active record id back
* in the parent container
*/
handleClick() {
this.props.set(this.props.recordId);
};
/**
* Render record based on props
*/
render() {
const record = Library.find(record => record.id === this.props.recordId);
return (
<div>
{record.title}
<button onClick={this.handleClick}>View</button>
</div>
);
}
}
/**
* Displays an active record
*/
function ActiveRecord({ activeRecordId }) {
const activeRecord = Library.find(record => record.id === activeRecordId);
return (
<div>
<h3>Active Record</h3>
<img width="100" src={activeRecord.image} />
<audio controls>
<source src={activeRecord.sound} type="audio/mp3" />
</audio>
</div>
)
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<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>
<div id="root"></div>
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)
}));
}