So I've got a vue component I'm using as an editing interface...
The database has a jsonified array inside of it (don't ask... I don't like it either), which has to be in a particular format... In order to allow admins to edit this string array without breaking the format, I'm making a vue editing component which will break the parts up into various textboxes etc...
I would like to have two separate variables - one for what the string array IS, and one for what they are changing... the first variable would be updated when they saved their changes...
The issue I have is that for some reason, when I update one of the variables... not only does the other variable change... but the prop changes as well...
I was under the impression that components could not change their props..?
In particular, the array looks something like this:
[
'1',
'2'
['1','2','3','4']
]
And when I do a .splice() on the sub-array, both variables and the prop updates...
Some sample code...
Laravel blade view:
<editor :somearray={{ $someJsonifiedArray }}></editor>
Component props/data setup:
props: {
somearray: {
default: [],
type: Array
}
}
data(){
return {
editedArray: this.somearray, // This is what will be saved
wipArray: this.somearray // This is what changes as they edit
}
}
Some of the methods:
resetChanges(){
this.wipArray = this.editedArray;
}
I'm probably missing something obvious... or misunderstanding how things work...
Javascript Array/Object is passed by reference, not by value!
if you do this
return {
editedArray: this.somearray, // This is what will be saved
wipArray: this.somearray // This is what changes as they edit
}
whenever you edit editedArray or wipArray, you are actually editing somearray, because they all refer back to the same array/Object .
So you gotta clone the array/Object instead of passing it directly. The simplest way to clone object is by using spread operator (or in some cases, deep cloning will be required). The simplest way to clone array is by using slice.
return {
editedArray: {...this.somearray}, // This is what will be saved
wipArray: {...this.somearray} // This is what changes as they edit
}
Related
I have an app where the redux state has a field called data that's null initially, then when uploading something it changes to an array of objects and the reducer uses delete to remove an object when we press the required button. But that doesn't change the length of the array so it causes an uncaught error. The simplest solution would be to simply change the length of the array after we delete that object. Can this produce any bugs?
EDIT: This is the code:
case actions.RIR:
const filtered = [...state.data];
delete filtered[action.payload];
filtered.length--; // this has been added by me
return {...state, data: [...filtered]};
The original code isn't mine. It's part of a project. I've been asked to fix an error.
First one foremost, reducers should be pure - meaning - no side effects.
delete is a side effect since it mutates an original object.
In reducers, use immutable patterns - either use filter, or splice a copy of the original array (less recomended).
Besides that, delete will simply make this element undefined, and your array will look like:
['item 1','item 2'....,empty,'item n']
I would write it:
case actions.RIR:
const filtered = state.data.filter(item => item !== action.payload);
return {...state, data: filtered};
Or whatever filter logic you might require.
I have 2 components:
parent component (vue-bootstrap modal with a vue-bootstrap table)
child component (vue-bootstrap modal with a form)
When I submit the form in the child component I want to push the object to the parent table array and that works! But when I reset the form it also resets the object in the table array and I don't know why. I tried push and concat.
parent variable:
MA02_E_tb // table array [{descr_forn: '',fornitore:'',n_oda:''},{descr_forn: '',fornitore:'',n_oda:''}]
data() {
return {
form: {
descr_forn: 'prova',
fornitore:'prova',
n_oda:'prova',
}
},
methods: {
resetModal() {
this.form.descr_forn = '',
this.form.fornitore = '',
this.form.n_oda = '',
},
onSubmit: function(evt) {
evt.preventDefault()
this.$parent.MA02_E_tb = this.$parent.MA02_E_tb.concat(this.form)
},
result:
MA02_E_tb = [{descr_forn: 'prova',fornitore:'prova',n_oda:'prova'}]
When I reopen the child modal and I reset the object form with the resetModal method the result is:
MA02_E_tb = [{descr_forn: '',fornitore:'',n_oda:''}]
form = [{descr_forn: '',fornitore:'',n_oda:''}]
Why does it reset MA02_E_tb if it's another variable?
First up, you really, really, really shouldn't be using $parent like that. You should be emitting an event instead. But that isn't what's causing the problem.
The actual problem is that you are passing an object by reference. If you then change that object it will be changed in both places. It doesn't matter what name, identifier or path you use to access it, it's still the same object.
Assuming it is a flat object you can make a shallow copy using the spread operator, ...:
this.$parent.MA02_E_tb = this.$parent.MA02_E_tb.concat({...this.form})
This will create a new object with the same properties as this.form. It is important to realise that this is only a shallow copy. If this.form contained further reference types (i.e. objects, arrays, etc.) then those may need copying as well.
As an event this would be something like:
this.$emit('submit', {...this.form})
You'd then need a suitable #submit listener in the parent template to update the array. The idea here is that only the owner of the data should be allowed to modify it and in this case that array is owned by the parent.
I'm currently trying to add a React component into the third layer of my multidimensional array. My structure is as follows:
constructor(props) {
super(props);
/**
* The state variables pertaining to the component.
* #type {Object}
*/
this.state = {
structure : [ // Block
[ // Slot 1
[ // Layer 1
<Text>text 1</Text>, // Text1
]
]
],
};
}
this.state.structure contains an array for the slots, which contains an array for the layers which contains an array consisting of Text components.
I've tried to use both concat() and push() to try to set the structure array, but both give me errors like "cannot read property of undefined", etc. This is my current addText() function:
addText() {
this.setState((previousState) => ({
structure : previousState.structure[0][0].concat(<Text>text2</Text>),
}));
}
The addText() function is called upon pressing a button in my application.
I'm also rendering my array of Text components on the current layer directly through the array:
{ this.state.structure[0][0] }
I feel like I'm staring the problem directly in the face, but have no idea what's causing the issue. I'm expecting to be able to add another Text to the container, but nothing I've tried to do seems to be working.
For one thing, you really should NOT store instances of React components directly in state. Doing so is just asking for trouble down the line, as rendering, state updates, key management and data persistence get more and more difficult to wrangle.
Instead, store a model of your components in state, generally in the form of the props they should have. Then, at render time is when you translate those models into React components:
this.state = {
structure : [ // Block
[ // Slot 1
[ // Layer 1
"text 1", // Text1
]
]
],
};
render() {
return this.state.structure[0][0].map(ea => <Text key={ea}>{ea}</Text>);
}
Doing it this way is better because: what happens if you want to read or modify the contents of your "layer" arrays? If they are text values you can simply read/modify the text. But if they are instantiated React components... there is basically no way to effectively update your state without jumping through tons of hoops.
On to your specific problem: the issue is that when you evaluate a statement like this:
previousState.structure[0][0].concat(<Text>text2</Text>)
the concat() function actually returns the value of the array after concatenation. And the value of that array is (in this case) now [<Text>text 1</Text>, <Text>text2</Text>]. So, you are actually updating the value of your state.structure field to an entirely different array. I'm guessing the error you are seeing - "cannot read property of undefined" - is because when you try to access this.state.structure[0][0] you are trying to access it as if it were a two dimensional array but it is now in fact a one dimensional array.
As another replier put it much more succinctly: you are entirely replacing state.structure with the contents of state.structure[0][0] (after concatenation).
Updating deeply nested data structures in React state is super tricky. You basically want to copy all of the original data structures and change only parts of it. Fortunately, with ES6 "spread" operator we have a shorthand for creating new arrays with all of the same items, plus new ones:
addText() {
this.setState((previousState) => {
// Copy the old structure array
const newStructure = [...previousState.structure];
// Update the desired nested array by copying it, plus a new item
newStructure[0][0] = [...newStructure[0][0], "text2"];
this.setState({structure: newStructure});
});
}
Main takeaways:
Don't store React components in state! Store raw data and use it at render time.
Be careful about what you are updating state to - it needs to be the SAME state plus a few updates, not the result of some atomic operation.
Read up a bit on how array operations like concat() and push() actually work and what their return values are.
I dont agree with this arr structure, and hooks, with an object seems much more conventional, but it could be your this binding. as ray said below structure[0][0] is replacing the state.
if your using addText Method it needs to be bound to the class. If its not using arrow syntax, it will show as undefined as it is not bound, and will result in undefined. Hope it helps
for example
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '',
};
this.addText = this.addText.bind(this);
}
addText() {
this.setState((previousState, props) => ({
structure : previousState.structure[0][0].concat(<Text>text2</Text>),
}));
}
// ES6
addTextEs6 = () => {
this.setState((previousState, props) => ({
structure : previousState.structure[0][0].concat(<Text>text2</Text>),
}));
}
}
//ES6
/* Also you do not need to use concat you can use the spread Op. ... See MDN Spread Op`
Without getting into any philosophy about whether you should do it the way you're doing it, the fundamental problem is that you're replacing structure with structure[0][0] in your state.
{
// this line sets the whole structure to structure[0][0]:
structure : previousState.structure[0][0].concat(<Text>text2</Text>),
}
I am currently working on a project in which I have to maintain an object that contains an array of smaller objects (collections) in js. Adding and editing seem to be working fine but I have hit a snag when trying to remove from the the object.
removeCollection(collectionId, index){
let copyCollectionItems = {...this.state.collectionItems};
copyCollectionItems[collectionId].splice(index, 1);
this.setState({ collectionItems: copyCollectionItems});
}
In the above code the object's state before the call looks something like:
{
collectionId-1: Array(4)[{...}, {...}, {...}, {...}],
collectionId-2: Array(2)[{...}, {...}],
collectionId-3: Array(1)[{...}]
}
Assuming the collectionId coming in is "collectionId-1", I expect the object to look like this after the call:
{
collectionId-1: Array(3)[{...}, {...}, {...}],
collectionId-2: Array(2)[{...}, {...}],
collectionId-3: Array(1)[{...}]
}
Instead I see that there are no changes to the object after the function call (collectionId-1 still has 4 objects in the array).
Is there something wrong with the way I am copying my object? Maybe the reference data is overwriting the new data? I have no idea why the object isn't changing.
I should also add that if the index coming in is 0, then the removal is successful. Any other index will not change the object.
I am using Ajax to populate data properties for several objects. As such, the properties I want to bind to do not exist at the time of binding.
eg:
<template>
<my-list v-bind:dataid="myobject ? myobject.data_id : 0"></my-list>
</template>
<script>
export default {
data () {
return {
myobject: {}
}
}
</script>
In the Vue docs https://012.vuejs.org/guide/best-practices.html it mentions to initialize data instead of using a empty object.
However I am using multiple Ajax created objects with tens of parameters and sub parameters. To initialize every sub-parameter on all objects like this:
myobject: { subp1: [], subp2: [] ...}
where myobject may be an object containing array of objects, or an array of objects containing sub-arrays of objects for example.
would take quite a bit of work. Is there a better alternative when binding to 'not-yet existing' objects?
First of all, an empty array is still "truthy", so your check here
v-bind:dataid="myobject ? myobject.data_id : 0"
always returns true. You should check for myobject.length instead. Your code should work now.
Also, you really don't need to define dummy objects for an array. Vue detects whenever you mutate an array.
https://v2.vuejs.org/v2/guide/list.html#Array-Change-Detection