Home > Software engineering >  Vue updating computed object, or updating the original?
Vue updating computed object, or updating the original?

Time:01-05

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>

CodePudding user response:

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.

CodePudding user response:

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.

  • Related