As much as I'd like to share just share the relevant code in a simplified version, I can't. I have a slightly overdone todo app made in Vue. Here's the link to the repo: https://github.com/jaiko86/subtasks
The todo items are managed in the store:
export default {
state: {
workspaceIds: [], // IDs of workspaces
tasksById: {}, // <--- this is the tasks I have
detachedTask: null,
focusedTaskId: null,
currentWorkspaceId: null,
},
...
}
In the App.vue file, I have the following computed property that the v-for will run through:
computed: {
taskIds() {
const { currentWorkspaceId, tasksById } = this.$store.state;
if (this.showOnlyLeafSubTasks) {
return Object.values(tasksById)
.filter(task => !task.subTaskIds.length)
.map(task => task.id);
} else if (currentWorkspaceId) {
return tasksById[currentWorkspaceId].subTaskIds;
}
},
},
Basically, the computed property will only return a list of tasks that are relevant to certain conditions, such as the workspace that I'm in. Each task has a unique ID, but for the sake of debugging, I've made it so that each task has the ID of the format task-#, and the task's input will have its task ID as a placeholder.
Here's the template in the App.vue:
<template>
<div id="app">
<!-- This is the top part -->
<WorkspaceNav />
<!-- this is the for-loop in question -->
<TaskWrapper v-for="id in taskIds" :key="id" :id="id" :depth="0" />
<!-- this is the filter that's on the right side -->
<TaskFilters />
</div>
</template>
The problem is that it won't render the items that the computed property returns.
I am failing miserably in trying to understand why there's a discrepancy between what's shown in the vue devtool and the console, and the view.
Here is the rendered view:
Here is what's in the vue dev tool:
Here's what's printed in the console when I select the <App> component in the dev tool, thereby making it accessible via $vm0.taskIds:
["task-1", "task-2"]
Here's my custom function that prints the tasks hierarchically:
> printTree()
task-0
task-1
task-2
Here's the DOM for the relevant section of the code for the moment:
<div id="app">
<div class="workspace-nav">
...
</div>
<div data-v-76850a4a="" data-v-7ba5bd90="" class="leaf">
<div data-v-76850a4a="" class="main-task depth-0">
<!---->
<div class="progress-indicator">
<div class="checkmark gray-checkmark"></div>
</div>
<input placeholder="task-1" />
</div>
<div class="sub-tasks"></div>
</div>
<div class="filters">
...
</div>
</div>
So it's clear that no matter where I look, taskIds is returning a list of two items, but it's only rendering the first one.
Here's one possible explanation...
At the point that the computed property returns its array the array only contains one item. You can confirm this in several ways. e.g. Try putting this in your template:
{{ taskIds }}
That array could subsequently change so that it contains two items but if the changes don't trigger the reactivity system them re-rendering will not occur.
Best guess is that tasksIds is returning tasksById[currentWorkspaceId].subTaskIds. That leaves two likely possibilities. Either subTasksIds isn't reactive, or it is being modified in a way that doesn't trigger the reactivity (e.g. direct modification by index).
Taking a look at your store code I don't see any obvious examples of the latter. However, this line from createTask does seem suspiciously like the former:
state.tasksById[task.id] = task;
This is adding task to an existing object under a (potentially) new key. This is one of the reactivity caveats, you can't add new properties to objects directly:
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
Instead you'd need to use Vue.set:
Vue.set(state.tasksById, task.id, task);
To better understand what's going on try adding the following logging to the end of createTask:
console.log(state.tasksById);
If you dig into the objects/arrays in the console you should see evidence of the Vue reactivity system. You may see references to Observer. Properties will have getters and setters and you need to click to see the property values. However, this will only happen if the object/array is reactive. Tasks that you add directly, without Vue.set, will just appear as normal objects/arrays in the console. It's worth taking some time to learn what the difference looks like as it makes debugging these kinds of problems much easier if you know what to look for.
Related
So I've been trying to figure this one out for a while and I'm really hoping someone would be able to help
I'm creating an array and storing a list of objects in it like this.
const heroPost = postList.filter(post => post.hero);
when I console log heroPost I get the following result.
I'm trying to access the url property in this object as follows.
console.log(heroPost[0].url);
But I keep getting this undefined error.
I'm not really sure what's going wrong here. I am using an async function to get data from a database which I'm guessing might be a problem but I'm outputting data from the 'postList' in jsx which I've used to retrieve data into the 'heroPost' so I'm uncertain how it can be the problem. Any help or ideas would be greatly appreciated.
Thank you!
Edit:
This is the portion of the component that uses the 'postList' to render data. Which works fine.
<div className="posts-container">
{postList.map((post) => (
<div key={post.id} className="post">
<h3 className="post-title">{post.title}</h3>
<div className="post-info">
<p className="author">{post.author}</p>
<p className="timestamp">{post.author}</p>
</div>
<p className="content">{post.body}</p>
<img src={post.url} alt="" />
</div>
))}
</div>
What I'm trying to do now is to use the heroPost to render data into the hero section of the page as follows but it is still throwing the undefined error.
<div className="hero">
<img src={heroPost[0].url} alt="" />
</div>
Thank you for all the comments and answers, I understand the problem now. Still don't really get why it works in the 'postList' instance and not here. Is there a way I can do this without directly trying to go to the file where I retrieve the data and add it into that function?
Since you are using an async function to get the data, when the component that consists this code, renders for the first time, it will not have the data it needs to render the component.
Hence 'undefined'
You can resolve this issue by conditionally rendering the component. That is, render the component only after the data is available.
Since you haven't shared the component code, you can refer this react doc to help with the issue : Conditional Rendering
Edit: following is how the condition would work. This is assuming postList is a state variable and a re-render will be triggered when it's value is changed:
For the second snippet you shared:
<div className="hero">
{(heroPost && heroPost[0])?<img src={heroPost[0].url} alt="" />:'No data'}
</div>
Simple answer: you get those error because it's undefined
If you look at you console.log you can see an empty array at the first line
That's because when you first render your component postList is an empty array.
Only after you fetch your data and populate you array you can access it
you can try to do this
useEffect(() => {
setHeroPost(postList.find(p => p.hero))
}, [postList])
I have been working on a Vue 2 project for a while, and upon upgrading our linting requirements I discovered that we had prop mutation errors in many of our child components. In our project, we pass a singleton object as a prop to many components and were originally updating the object directly from the child components. Vue seems to suggest using the v-bind.sync feature for updating props from child components (or using the equivalent v-bind and v-on). This, however, doesn't solve the issue of prop modification from nested components in an array.
Take this (pseudo)code for example that uses prop mutation:
Note: Assume const sharedObject: { arrayElements: Array<{ isSelected: boolean }> } = ...
Page.vue
<template>
...
<Component1 :input1="sharedObject" />
...
</template>
Component1.vue
<template>
...
<template v-for="elem in sharedObject.arrayElements">
<Component2 :input2="elem" />
</template>
...
</template>
Component2.vue
<template>
...
<q-btn #click="input2.isSelected = !input2.isSelected"></q-btn>
...
</template>
What is the proper way of updating a property like input2.isSelected from nested components in Vue 2? All of the approaches I've thought of are flawed.
Flawed Approaches
I believe that we would like to bubble up that input2.isSelected has been modified in Component2 to Page.vue, however, this seems to either lead to messy code or an uneasy feeling that we are just suppressing linting errors in a roundabout way.
To demonstrate the "messy code" approach, first note that Page.vue does not know the index of of elem in sharedObject.arrayElements. Therefore, we would need to emit an object to Page.vue from Component1 which contains the state of input2.isSelected as well of the index of elem in sharedObject.arrayElements. This gets messy quickly. What about the example where we have:
Component1.vue
<template>
...
<template v-for="elem in sharedObject.arrayElements">
<template v-for="elem2 in elem.arrayElements">
<Component2 :input2="elem2" />
</template>
</template>
...
</template>
in this case, then we could need to pass up 2 indices! It doesn't seem like a sustainable solution to me.
The alternative that I thought of is a callback function (passed as a prop through the component hierarchy) that takes as input the element we want to update and an object that contains the properties we want to update (using Object.assign).
This makes me very uneasy since I don't know the real reason why we can't update a pass-by-reference prop from a child component. To me, it seems like it's just a roundabout way of updating the passed-in from Component2 without the linter noticing. If there is some magic modification that happens to props when they're passed to child components, then surely passing in the object that I received in Component2 to the callback function and modifying it in the parent component would basically just be updating the prop in the child component, but more complicated.
What is the proper way of approaching this problem in Vue 2?
Very good question and analysis of the current state of this long-standing issue in Vue ecosystem.
Yes, modifying "value type" props from the child is a problem as it creates runtime issues (parent overwriting the changes when re-rendered) and thus Vue generates a runtime error when this happens...
Modifying a property of object passed as prop is OK from the "code works fine" POV. Unfortunately there are some influential people in the community with opinion (and many who blindly follow them) that this is an anti-pattern. I do not agree with that and raised my arguments many times (for example here). You described the reasons very well - it just creates unnecessary complexity/boilerplate code...
So what you are dealing with is really just a linting rule (vue/no-mutating-props). There is an ongoing issue/discussion that proposes the configuration option that should allow to ease the strictness of the rule with many good arguments but it gets very little attention from the maintainers (feel free to raise your voice there too)
For now what you can do is:
Disable the rule (far from perfect but luckily thanks to a Vue runtime error you can catch the real incorrect cases during development just fine)
Accept the reality and use workarounds
Workaround - use global state (store like Vuex or Pinia)
Note: Pinia is preferred as next version of Vuex will have same API
General idea is to place the sharedObject in the store and use props only to navigate the child components to the right object - in your case the Component2 will receive an index by prop and retrieve the right element from the store using it.
Stores are great for sharing the global state but using is just to overcome the linting rule is bad. Also as a result, components are coupled to the store hence both reusability suffers and testing is harder
Workaround - events
Yes it is possible to create mess and lot of boilerplate code using only events (especially if you nest components more than 2 levels) but there are ways to make things cleaner.
For example in your case Component2 does not need to know the index as you can handle the event like this
// Component1
<template>
...
<template v-for="elem in sharedObject.arrayElements">
<template v-for="(elem2, index) in elem.arrayElements">
<Component2 :input2="elem2" #update="updateElement($event, index)" />
</template>
</template>
...
</template>
In your case, the Component2 handles only change of single boolean property so $event can be simple boolean. If there are more than one property to be changed inside Component2, $event can be an object and you can use object spread syntax to "simplify" the Component2 (using one event instead of multiple - one for each property)
// Component2
<template>
...
<input v-model="someText" type="text">
<q-btn #click="updateInput('isSelected', !input2.isSelected)"></q-btn>
...
</template>
<script>
export default {
props: ['input2'],
computed: {
someText: {
get() { return this.input2.someText },
set(newVal) { updateInput('someText', newVal) }
}
},
methods: {
updateInput(propName, newValue) {
const updated = { ...this.input2 } // make a copy of input2 object
updated[propName] = newValue // change selected property
this.$emit('update', updated) // send updated object to parent
}
}
}
</script>
Well...I prefer just to disable the rule and set some clear naming conventions to indicate that the component is responsible for changing it's input...
Note that there are other workarounds like using this.$parent, inject\provide or event bus but those are really bad
Essentially I have an object whose keys and values are altered via other functions.
app=new Vue({
...
data:{
myObject:{"key1":"value1","key2":"value2"}
}
})
I wrote a component which receives them as props and displays them.
Vue.component("my-component",{
props:['k','v'],
template:`
<div>{{k}}:{{v}}</div>
`
})
Now, when I write this:
<my-component v-for="(value,key) in myObject" v-bind:k="key" v-bind:v="value"></my-component>
And execute app.myObject['someKey']='some value' nothing happens and without any console message, even when in development mode.
Where am I going wrong? Or is there a better way to render an object's key and values via a component?
Edit:
Something I observed, when the object is already is populated in the data object, it renders perfectly. However when an outside function modifies it, the changes are not reflected unless I modify a key's value which value which was already present in the object.
Edit2:
https://jsfiddle.net/agentrsdg/xs635ndk/8/
Loop for children in Object like below.
<div v-for="(value, key, index) in object">
So for your codes.
<my-component
v-for="(value, key) in myObject"
v-bind:key-prop="key" // prop name in my-component is keyProp but use key-prop in the parent component
v-bind:value-Prop="value"
/>
Here is the JSFiddle for my answer.
So I thought I discovered a bug and an issue on github(https://github.com/vuejs/vue/issues/10611), and they were quick to respond with the solution. The above not working is a limitation of JavaScript itself. So one should use,
app.$set(app.myObject,'newKey','newValue)
or
Vue.set(app.myObject,'newKey','newValue')
More details here : https://v2.vuejs.org/v2/guide/list.html#Object-Change-Detection-Caveats
I am trying to get my head wrap around how props are passed to child components and how they are updated from there.
// parent.vue
<template>
<div>
<div v-for="elem in getCurrentArray">
<child-component :elem="elem"></child-component>
</div>
<button #click.prevent="currentIdx--">Prev</button>
<button #click.prevent="currentIdx++">Next</button>
</div>
</template>
<script>
export default {
components : {
ChildComponent
},
data(){
return {
arr : [ ["A", "B", "C" ], [ "D", "E" ]], // this would be populated from vue store
currentIdx : 0
}
},
computed : {
getCurrentArray(){
return this.arr[this.currentIdx]
}
},
}
</script>
// child.vue
<template>
<div>Prop: {{elem}} <input type="text" v-model="myinput" #blur="save" /></div>
</template>
<script>
export default {
props : [ "elem" ],
data(){
return {
myinput : this.elem
}
},
methods : {
save(){
// this.$store.dispatch("saveChildElement", { myinput }
}
},
mounted(){ console.log( this.elem + " being rendered" ) }
}
</script>
In this example, I have two sets of arrays ['A','B','C'] and ['D','E']. On page load, the first set is rendered thru child components.
Looks good. However, when I click next to go to the second set, I get this:
So, while the props are being passed correctly, the textbox input values are not updated. When I checked the console.log, it's clear that vue does not re-render child components for "D" and "E". Instead, it simply uses the existing components for "A" and "B".
Is there a way to force vue to re-render the components? If not, how can I make sure that the textbox input gets the latest prop values? Keep in mind that I want to be able to save changes to the input values via vue store.
Add a key as cool-man says, it will fix the issue.
However, your check for component re-rendering is flawed, you used the child mounted life cycle, and because you see it running only once you think the component isn't re-rendering. But this is wrong. "mounted" happens only once in a component life cycle, and because you're using the same component for ['A','B','C'] and then for ['D','E'] vue knows not to re-create the components and simply re-renders them with the new props.
Try to add the :Key prop in the v-for loop
<div v-for="elem in getCurrentArray" :key="elem">
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item:
... List Rendering - Maintaining State
Keys must be unique, if you have two or more same value in your array, for example ['A',' B', 'A'] it will create a conflict, the best way to overcome this is to add a generated unique 'ID' for each instance.
Or In alternative (a short term solution)
You can use the loop index in combination with the array value, this should give you a more or less unique key.
<div v-for="(elem, index) in getCurrentArray" :key="elem + index">
You can ask your component to re-render. (The other answers of updating the key is correct).
However, if you do want to use Vuex to manage state, I suggest you do that first as there is (essentially) a different mechanism that will do that re-render for you, and feel much easier. Namely the “computed properties” will likely solve that for you.
I remember getting stuck on this when I was starting out, and once I implemented Vuex I wished I had just done it that way, which kind of just solved the problem for me as part of its workflow. (It will likely mean you don’t need to update key, but you have that in case you have a complex situation when you still need to force a re-render).
Edit:
As #JonyB rightly pointed out, you are trying to re-render the whole component. If you think about it, what you really want to do here is not re-render the whole component, but merely update the state. Therefore, once you implement Vuex, it will likely solve this for you as it allows you to deal with state separately.
All you need to add the key to the v-for loop and it will work.
Replace
<div v-for="elem in getCurrentArray">
with
<div v-for="elem in getCurrentArray" :key="elem">
Ref: https://v2.vuejs.org/v2/guide/list.html#v-for-with-a-Component
When I am building a component, some times, computed properties are getting very large and hard to read. For example, when I have a component that handles a form with 10 fields (inputs, selects etc) I usually create 10 computed properties that checks the validation of the correspond form-field. 10 computed properties that watches for changes through v-models and so on.
What I want is if anyone know a way to create a custom property and extend the computed functionality
For example:
<template>
<div>
<input class="form-control" v-model="$options.vmodels.firstName">
</div>
</template>
and the script part
export default {
name: 'ComponentA',
computed: {},
vmodels: {
firstName:{
get(){ return store.state.firstName}, // lets assume this works
set(value){store.state.firstName = value}
}
},
methods: {},
watch: {},
}
I tried to use $options to call custom properties but it doesn't seem to work for computed functionallity. For example if I want to create a computed custom property with get/set and pass it to v-model then it will never mutate to fire the set or get method.
The main reason I want that is to increase code readability. And yes I know many of you should say that I should create more than one components if the component gets very big but still I want to know if the above approach can work
Thanks in advance!
What I understand is that you would like to inherit some properties to use in your ComponentA. This is the exactly usage of Vue Mixins. You can create another file for your mixin for the fields of your form with computed properties as you have been using, and add that mixin in your ComponentA.