Vue 3, composition API, Array of refs doesn't work - javascript

Please see below code.
<template>
<div v-for="item in arr" :key="item">{{ item }}</div>
</template>
<script>
import { ref } from "vue";
export default {
name: "TestArr",
setup() {
const arr = [];
arr.push(ref("a"));
arr.push(ref("b"));
arr.push(ref("c"));
return { arr };
}
};
</script>
And the output is below
{ "_rawValue": "a", "_shallow": false, "__v_isRef": true, "_value": "a" }
{ "_rawValue": "b", "_shallow": false, "__v_isRef": true, "_value": "b" }
{ "_rawValue": "c", "_shallow": false, "__v_isRef": true, "_value": "c" }
expected output
a
b
c
I have to call item.value in the template to make it work.
What's the work around for this scenario in vue3?
Cheers!

You are doing it wrong; try following
setup() {
const arr = ref([]);
arr.value.push("a");
arr.value.push("b");
arr.value.push("c");
return { arr };
}
There is no point adding ref items to a normal array. The Array itself should be ref.

Some information about using array with ref() and reactive() which may be helpful.
Recently, I am learning composition API by developing a simple todo list app. I ran into some problems when dealing with array by using ref() and reactive() and found some behaviors which may be helpful for folks who are learning composition API too, so I wrote down some words here. If there is something wrong, please tell me!
1. What is the problem when I use reactive() to deal with array?
So...at first everything just work as I expected until I working on developing delete function.
I tried to build a button which will trigger the deleteHandler function when it been click. And the deleteHandler would filter out the element in todos:
Here is my code:
<template>
<div>
<h1>reactive</h1>
<button #click="add">click</button>
<div v-for="item in todos" :key="item">
<button #click="mark(item)">mark</button>
<span>{{item}}</span>
<button #click="deleteHandler(item.id)">delete</button>
</div>
</div>
</template>
<script>
import {reactive, ref} from "vue";
export default {
name: "ReactiveMethod",
setup(){
let todos = reactive([])
const id = ref(0);
function add(){
todos.push({id:id.value, name:"hallo", state:"undone"});
id.value += 1
}
function mark(item){
if(item.state === "undone"){
item.state = "done"
}else{
item.state = "undone"
}
}
function deleteHandler(id){
const temp = todos.filter((element)=>{
return element.id !== id
})
todos = temp
}
return {
todos,
id,
deleteHandler,
add,
mark
}
}
}
</script>
However, I face a crucial problem, since the filter function would not mutate the original value but return a new value. Vue could not detect the change inside todos.
To solve this problem, I rewrite my code. Instead of assigning todos to reactive([]), I warpped the array with object like this -> reactive({todos:[]}). And it works !
<template>
<div>
<h1>reactive</h1>
<button #click="add">click</button>
<div v-for="item in todos" :key="item">
<button #click="mark(item)">mark</button>
<span>{{item}}</span>
<button #click="deleteHandler(item.id)">delete</button>
</div>
</div>
</template>
<script>
import {reactive, ref, toRefs} from "vue";
export default {
name: "ReactiveMethod",
setup(){
const state = reactive({
todos:[]
})
const id = ref(0);
function add(){
state.todos.push({id:id.value, name:"hallo", state:"undone"});
id.value += 1
}
function mark(item){
if(item.state === "undone"){
item.state = "done"
}else{
item.state = "undone"
}
}
function deleteHandler(id){
const temp = state.todos.filter((element)=>{
return element.id !== id
})
state.todos = temp
}
return {
...toRefs(state),
id,
deleteHandler,
add,
mark
}
}
}
</script>
conclusion
It seems that vue could only watch on the change with same reference(object in JavaScript is called by reference), but could not detect the change when the reference is changed. As a resault, I think "wrap the array inside object" is a better way to deal with array in composition API.
2. ref() for primitive value and reactive() value?
According to the most information we could found, It seems that we can make a conclusion:
ref() for primitive value and reactive() value
However, if we write some code like this, Vue is still able to detect the change inside it:
const obj = ref({name:"charles"});
return{
...toRefs(obj)
}
The reason is that when we pass data into ref(), it would first check whether the data been sended is primitive or object. If it is object, ref() would call reactive() to deal with it.In other words, reactive() is the one who actually take on the job behind the scene.
little conclusion
At this stage, it seems that we can use ref() anytime. However, I think it's better to use reactive() for object and ref() for primitive to make difference!(If you have any ideas about this topic, please share it to me !)

This is the correct answer
setup() {
const arr = ref([]);
arr.value.push("a");
arr.value.push("b");
arr.value.push("c");
console.log(arr.value)
return { arr };
}
This option is possible, but the first is much better.
const arr = reactive([]);
arr.push("a")
arr.push("b")
arr.push("c")
console.log(arr)

They should be accessed using value field :
setup() {
const arr = [];
arr.push(ref("a").value);
arr.push(ref("b").value);
arr.push(ref("c").value);
return { arr };
}
but this is a bad practice, your should define your array as ref then push values to it :
setup() {
const arr = ref([]);
arr.value.push("a");
arr.value.push("b");
arr.value.push("c");
return { arr };
}
another crafted solution is to init the array with that values :
setup() {
const arr = ref(["a","b","c"]);
return { arr };
}

Related

Vue updating computed object, or updating the original?

I'm using the computed() method to add some data to a ref() array of objects.
Using this computed array of objects works for reading data, for example using v-for, but it is ignored (nothing happens) when I'm trying to update the data, the example below shows the working vs not working code.
In the useCart composable (see code below), I created a computedCartItems which maps the cart and adds the totalPrice for each item. Now in my index.vue file, I try to increase the amount for a cartItem, this works if I loop over the cart using <div v-for="cartItem in cart"> but it is ignored when using the computed object <div v-for="cartItem in computedCartItems">
useCart.js
const useCart = () => {
const cart = ref([])
const computedCartItems = computed(() => {
return cart.value.map(cartItem => {
return {
...cartItem,
totalPrice: cartItem.amount * cartItem.price
}
})
})
return {
cart,
computedCartItems,
}
}
export default useCart
index.vue (not working, using computed 'computedCartItems' object)
<div v-for="cartItem in computedCartItems">
<div>
<div>{{ cartItem.name }}</div>
<button #click="onIncrement(cartItem)">+</button>
</div>
</div>
<script setup>
const { cart, computedCartItems } = useCart()
const onIncrement = ({ id }) => {
const shoppingCartItemIndex = computedCartItems.value.findIndex(item => item.id === id)
computedCartItems.value[shoppingCartItemIndex].amount++
}
</script>
index.vue (working, using original 'cart' object)
<div v-for="cartItem in cart">
<div>
<div>{{ cartItem.name }}</div>
<button #click="onIncrement(cartItem)">+</button>
</div>
</div>
<script setup>
const { cart, computedCartItems } = useCart()
const onIncrement = ({ id }) => {
const shoppingCartItemIndex = cart.value.findIndex(item => item.id === id)
cart.value[shoppingCartItemIndex].amount++
}
</script>
TLDR; you're updating values on a copy of your original object. They are not linked so the original object doesn't receive the updated value.
Detailed anwser
Computeds are readonly. They are derivated data and should not be updated.
Because this is javascript, you can update the object attributes by reference, but you really shouldn't, this is a bad practise leading to unclear side effects.
See the typescript type of computed:
export declare interface ComputedRef<T = any> extends WritableComputedRef<T> {
readonly value: T;
[ComputedRefSymbol]: true;
}
So myComputed.value is readonly and cannot be assigned another value. You can still do myComputed.value.myProperty = 'foo' but, as mentioned, this is a bad practise.
More information on this on the official documentation
A possible solution
Create the totalPrice composable for each item, not for the entire cart, and assign the computed inside your item object.
const useItem = (reactiveItem) => {
const totalPrice = computed(() => reactiveItem.amount * reactiveItem.price)
// Assign a new property in your item, which is the derived totalPrice
reactiveItem.totalPrice = totalPrice
return reactiveItem
}
const useCart = () => {
const cart = ref([])
// Export a custom function to include the item and make it reactive + use composable (saves the final client from doing it)
const addItem = (item) => {
cart.value.push(useItem(reactive(item)))
}
return { cart, addItem }
}
const { cart, addItem } = useCart()
function createItem() {
addItem({ amount: 5, price: 10 })
}
Check this online playground with a working example.
I'm sure there are other ways of doing it, this is only one. You could use watch to react for your cart changes for example.
The Core Issue
A computed ref is derived data: it represents your data in some way; you do not update it directly, you update its sources.
There is a section about this in the docs which explains the issue quite succinctly:
Avoid mutating computed value
The returned value from a computed property is derived state. Think of it as a temporary snapshot - every time the source state changes, a new snapshot is created. It does not make sense to mutate a snapshot, so a computed return value should be treated as read-only and never be mutated - instead, update the source state it depends on to trigger new computations.
In your non-working example, you are not trying to update the actual computed ref (which is not even possible; see the doc references at the end of the answer); you are updating properties of the ref's value, which you can -- but shouldn't -- do. However, aside from all the other problems, the computed will not update, as the total price is based on the original item in cart, not the one in the computed, meaning an update is never triggered (as cart is not changed).
If you instead modify the source ref (cart), the computed ref will update and the example will work:
<!-- Use `computedCartItems` here -->
<div v-for="cartItem in computedCartItems">
<div>
<div>{{ cartItem.name }}</div>
<button #click="onIncrement(cartItem)">+</button>
</div>
</div>
<script setup>
const { cart, computedCartItems } = useCart()
const onIncrement = ({ id }) => {
// Use `cart` here.
const shoppingCartItemIndex = cart.value.findIndex(item => item.id === id)
cart.value[shoppingCartItemIndex].amount++
}
</script>
A (Possibly) Better Way
While this works, it is quite possibly not the ideal way to go about solving your particular case. Every time an item is updated, the whole computed array and every item in it is recreated, which is very inefficient.
Instead, you can make the useCart composable only return the single cart ref along with some methods to manipulate the cart. You could do something like this:
import { ref, reactive, computed, readonly } from 'vue'
const useCart = () => {
const cart = ref([])
/**
Add a new item to the cart.
Makes the item reactive (so that there is a reactive source for computed properties),
adds the `totalPrice` computed property, and appends it to the cart array.
*/
const addItem = (item) => {
const reactiveItem = reactive(item)
reactiveItem.totalPrice = computed(() => reactiveItem.amount * reactiveItem.price)
cart.value.push(reactiveItem)
}
/**
Increase the amount of an item.
You could add all kinds of methods like these.
*/
const increaseAmount = (id) => {
const index = cart.value.findIndex((item) => item.id === id)
cart.value[index].amount += 1
}
return {
cart: readonly(cart), // So that the cart cannot be modified directly by the consumer.
addItem,
increaseAmount
}
}
const { cart, addItem, increaseAmount } = useCart()
addItem({ id: "1", amount: 5, price: 10 })
console.log(cart.value[0].totalPrice) // 50
Now the handling of the cart is done by the useCart composable, making things easier for the consumer by abstracting away internals. In addition to the gains mentioned above, this also means that the composable remains in control of its data, as the cart ref cannot just be modified. "Separation of concerns", etc.
Documentation References and Such
Vue Docs
Computed Properties - Vue.js Docs
The whole point of computed refs is that they update automatically based on their sources. You do not modify them directly, you modify their sources.
A computed property automatically tracks its reactive dependencies. Vue is aware that the computation of publishedBooksMessage depends on author.books, so it will update any bindings that depend on publishedBooksMessage when author.books changes.
You cannot assign a value to a regular computed ref.
Computed properties are by default getter-only. If you attempt to assign a new value to a computed property, you will receive a runtime warning. In the rare cases where you need a "writable" computed property, you can create one by providing both a getter and a setter.
I highly recommend reading the "Reactivity Fundamentals" section of the Vue Guide. See especially "Ref Unwrapping in Reactive Objects" for some insight on how the nesting of the computed ref inside the reactive works.
I also suggest going through the entire "Reactivity in Depth" page when you're ready. It gives you a grip on how the reactivity system actually works.
Other Links
VueUse is a great resource, both for many handy composables and for learning.

Vue composition API renders {value: 0} instead of plain value for nested field

I'm trying to create a component that can be used via destructuring and as js object.
Destructuring works fine, but with js object, I'm forced to write obj.field.value instead of just obj.field. Is it expected behavior?
I'm using composition-api plugin.
Playground: https://jsfiddle.net/3mzve8oy/21/
<div id="app">
<h2 #click="ctx.fn">{{ ctx.counter }} {{counter}} </h2>
</div>
const { reactive, toRefs } = VueCompositionAPI;
const useCounter = () => {
const ctx = reactive({
counter: 2,
// ...
});
const fn = () => {
console.log('Called', ctx.counter);
}
setInterval(() => ctx.counter += 1, 1000);
return {...toRefs(ctx), fn};
}
new Vue({
el: "#app",
setup() {
const ctx = useCounter();
const { counter } = useCounter();
return { ctx, counter };
}
})
Expected output: 0 0
Actual output: { "value": 0 } 0
Yes, this is expected behavior. See Ref Unwrapping docs, which describe your exact scenario:
When a ref is returned as a property on the render context (the object returned from setup()) and accessed in the template, it automatically shallow unwraps the inner value. Only the nested ref will require .value in the template:
<template>
<div>
<span>{{ count }}</span>
<button #click="count ++">Increment count</button>
<button #click="nested.count.value ++">Nested Increment count</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return {
count,
nested: {
count
}
}
}
}
</script>
This is my first time seeing vue-composition-api, but in their docs it looks like they use value as design choice in objects to preserve reactivity on the object if it gets passed around the app.
see here:
https://v3.vuejs.org/guide/composition-api-introduction.html#setup-component-option
ref takes the argument and returns it wrapped within an object with a value property, which can then be used to access or mutate the value of the reactive variable:*
import { ref } from 'vue'
const counter = ref(0)
console.log(counter) // { value: 0 }
console.log(counter.value) // 0
counter.value++
console.log(counter.value) // 1

How do I create a new JSON object inside a react hook?

I have two issues first how do I add/update the JSON items within a hook?
The other being that React won't let me use the name stored from a previous JSON file.
I am open to other solutions, basically, as my input field are dynamically generated from a JSON file I'm unsure of the best way to store or access the data that's input into them I think storing them in a react hook as JSON and then passing them though as props to another component is probably best.
What I want to happen is onChange I would like the quantity value to be stored as a JSON object in a Hook here's my code:
React:
import React, { useState, useEffect } from 'react';
import Data from '../shoppingData/Ingredients';
import Quantities from '../shoppingData/Quantities';
const ShoppingPageOne = (props) => {
//element displays
const [pageone_show, setPageone_show] = useState('pageOne');
//where I want to store the JSON data
const [Quantities, setQuantities] = useState({});
useEffect(() => {
//sets info text using Json
if (props.showOne) {
setPageone_show('pageOne');
} else {
setPageone_show('pageOne hide');
}
}, [props.showOne]);
return (
<div className={'Shopping_Content ' + pageone_show}>
//generates input fields from JSON data
{Data.map((Ingredients) => {
const handleChange = (event) => {
// this is where I'd like the Hook to be updated to contain instances of the ingredients name and quantity of each
setQuantities(
(Ingredients.Name: { ['quantities']: event.target.value })
);
console.log(Quantities);
};
return (
<div className="Shopping_input" key={Ingredients.Name}>
<p>
{Ingredients.Name} £{Ingredients.Price}
</p>
<input
onChange={handleChange.bind(this)}
min="0"
type="number"
></input>
</div>
);
})}
<div className="Shopping_Buttons">
<p onClick={props.next_ClickHandler}>Buy Now!</p>
</div>
</div>
);
};
export default ShoppingPageOne;
JSON file:
//Json data for the shopping ingredients
export default [
{
Name: 'Bread',
Price: "1.10",
},
{
Name: 'Milk',
Price: "0.50",
},
{
Name: 'Cheese',
Price: "0.90",
},
{
Name: 'Soup',
Price: "0.60",
},
{
Name: 'Butter',
Price: "1.20",
}
]
Assuming your Quantities object is meant to look like:
{
<Ingredient Name>: { quantities: <value> }
}
you need to change your handleChange to look like this
const handleChange = (event) => {
setQuantities({
...Quantities,
[Ingredients.Name]: {
...(Quantities[Ingredients.Name] ?? {}),
quantities: event.target.value
}
});
};
Explanation
When updating state in React, it is important to replace objects rather than mutating existing ones, as this is what tells React to rerender components. This is commonly done using the spread operator, and with array functions such as map and filter. For example:
const myObject = { test: 1 };
myObject.test = 2; // Mutates existing object, wrong!
const myNewObject = { ...myObject, test: 2 }; // Creates new object, good!
Note the spread operator doesn't operate below the first level, what I mean by that is, objects within the object will be copied by reference, for example:
const myObject = { test : { nested: 1 } };
const myObject2 = { ...myObject };
myObject2.test.nested = 2;
console.log(myObject.test.nested); // outputs 2
Also in my answer, I have used the nullish coalescing operator (??), this will return it's right operand if the left operand is null or undefined, for example:
null ?? 'hello'; // resolves to "hello"
undefined ?? 'world'; // resolves to "world"
"foo" ?? "bar"; // resolves to "foo"
In my answer I used it to fallback to an empty object if Quantities[Ingredients.Name] is undefined.
Finally, I used square brackets when using a variable as an object key as this causes the expression to be evaluated before being used as a key:
const myKey = 'hello';
const myObject = {
[myKey]: 'world';
};
console.log(myObject); // { hello: 'world' }

How to access child key-values on objects and clonate that object

I'm tryin to make a list from an object on React. Before I continue, I'll share my code:
const genres = {
Rock: {
album1: '',
album2: '',
},
Jazz: {
album1: '',
album2: '',
},
Pop: {
album1: '',
album2: '',
}
};
let myFunc = genress => {
let newObject = {};
Object.keys(genress).map(gen => {
newObject[gen] = 'a';
let newChild = newObject[gen];
let oldChild = genress[gen];
Object.keys(oldChild).map(gen2 => {
newChild[gen2] = 'b';
let newGrandChild = newChild[gen2];
console.log(newGrandChild);
})
});
return newObject;
}
myFunc(genres);
I wanna render that object on a list.
<ul>
<li>Rock
<ul>
<li>album1</li>
<li>album2</li>
</ul>
</li>
</ul>
...And so on
Before placing it on React I'm trying it on a normal function. I'm making a new object just to make sure I'm accesing the right values. The problem is, when I return the new object at the end of the function it returns the genres but not the albums, only the 'a' I set in the first Object.key. The console.log on the second Object.key logs undefined, and can't figure out why this is happening.
My idea is to have access to every level on the object so I can set them to variables and return them on the render's Component. I'll make more levels: Genres -> Bands -> Albums -> songs.
Thanks so much in advance :)
From what I can understand is that you are iterating over the object incorrectly.
The reason why 'a' is the only thing showing up is that you are hard coding that every time you run the loop and setting that current key that that value.
So essentially your code does not work because you set the value of the current key to be 'a' which is a string so there are no keys on 'a' so the second loop does not produce anything.
newObject[gen] = 'a'; // you are setting obj[Rock]='a'
let newChild = newObject[gen]; // this is just 'a'
let oldChild = genress[gen]; // this is just 'a'
Object.keys(newObject[gen]) // undefined
What I think you are trying to do is iterate over the object and then render the contents of that object in a list.
Let me know if the below answers your question.
You can see the code working here:
https://codesandbox.io/s/elastic-dhawan-01sdc?fontsize=14&hidenavigation=1&theme=dark
Here is the code sample.
import React from "react";
import "./styles.css";
const genres = {
Rock: {
album1: "Hello",
album2: "Rock 2"
},
Jazz: {
album1: "",
album2: ""
},
Pop: {
album1: "",
album2: ""
}
};
const createListFromObject = (key) => {
return (
<div>
<h1>{key}</h1>
<ul>
{Object.entries(genres[key]).map(([k, v], idx) => (
<li key={`${key}-${k}-${v}-${idx}`}>{`Key: ${k} Value ${v}`}</li>
))}
</ul>
</div>
);
};
export default function App() {
return (
<div className="App">{Object.keys(genres).map(createListFromObject)}</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>

How to handle multiple array filters with Vue JS?

Not sure when to use computed properties vs watch to display my data. I'm building an app with the PokeAPI, and I want to toggle between Type and Generation to display the pokemon. So far I have a JS file that stores all the pokemon in an array:
//pokeData.js
import axios from 'axios'
const allPokes = [];
export default{
getPokemon(){
if(allPokes.length === 0){
for(let i=1; i<=809; i++){
axios.get(`https://pokeapi.co/api/v2/pokemon/${i}`)
.then(response => {
allPokes.push(response.data);
allPokes.sort((a, b) => a.id - b.id);
});
}
}
return allPokes
}
}
I don't want to repeatedly call 809 Objects from an API, so I call them in the mounted() in my Vue, and want to filter them from there:
//Container.vue
//takes two props, 'currentType' and 'currentGen', to use to filter the pokemon
<template>
<div
v-for="(pokemon, index) in allPokemon"
:key="index">
<h2>{{ pokemon.name }}</h2>
</div>
</template>
<script>
import Pokemon from '../pokeData'
export default {
props: ['currentType', 'currentGen'],
data(){
return{
allPokemon: [],
}
},
mounted(){
this.allPokemon = Pokemon.getPokemon();
},
watch: {
currentType: function(newType){
const typePokes = this.allPokemon.filter(pokemon => {
if(pokemon.types[0].type.name == newType){
return true
}
this.allPokemon = typePokes
});
I know this is wrong, but I don't know how to fix it. I know you can use List Rendering as suggested in the official documents, but it doesn't say how to use it for multiple filters. https://v2.vuejs.org/v2/guide/list.html#Replacing-an-Array
Any advice is welcome: how to better cache the initial API call; to use watch or computed...
A computed prop is what you need in this situation:
computed: {
filteredPokemons () {
if (this.currentType) {
return this.allPokemon.filter(pokemon => pokemon.types[0].type.name == this.currentType)
}
if (this.currentGen) {
// I assumed that a prop is named 'gen', just replace with an actual prop
return this.allPokemon.filter(pokemon => pokemon.types[0].gen.name == this.currentGen)
}
// you didn't mentioned what to show when both currentType and currentGen is empty
// I assumed it should be an empty array
return []
}
}

Categories

Resources