DEV Community

YCM Jason
YCM Jason

Posted on

Thought on Vue 3 Composition API - `reactive()` considered harmful

Vue.js stands out from other frameworks for its intuitive reactivity. Vue 3 composition api is going to removing some limitations in Vue 2 and provide a more explicit api.

Quick Intro to the Composition API

There are two ways to create reactive "things":

  1. reactive()
  2. ref() / computed()

Introducing reactive()

reactive(obj) will return a new object that looks exactly the same as obj, but any mutation to the new object will be tracked.

For example:

// template: {{ state.a }} - {{ state.b }}
const state = reactive({ a: 3 })
// renders: 3 - undefined

state.a = 5
state.b = 'bye'
// renders: 5 - bye
Enter fullscreen mode Exit fullscreen mode

This works exactly like data in Vue 2. Except we can now add new properties to them as reactivity is implemented with proxies in Vue 3.

Introducing Ref

Vue composition API introduced Ref which is simply an object with 1 property .value. We can express this using Typescript:

interface Ref<A> {
  value: A
}
Enter fullscreen mode Exit fullscreen mode

There are two ways of creating refs:

  1. ref()
    • .value can be get/set.
  2. computed()
    • .value is readonly unless a setter is provided.

For Example:

const countRef = ref(0) // { value: 0 }
const countPlusOneRef = computed(() => countRef.value + 1) // { value: 1 }
countRef.value = 5

/*
 * countRef is { value: 5 }
 * countPlusOneRef is { value: 6 } (readonly)
 */
Enter fullscreen mode Exit fullscreen mode

reactive() is bad; Ref is good.

This section of the article is purely my tentative opinion on the composition api after building a few projects with it. Do try it yourself and let me know if you agree.

Before using the composition api, I thought reactive() would be the api that everyone will end up using as it doesn't require the need to do .value. Surprisingly, after building a few projects with the composition api, not once have I used reactive() so far!

Here are 3 reasons why:

  1. Convenience - ref() allow declaration of new reactive variable on the fly.
  2. Flexibility - ref() allow complete replacement of an object
  3. Explicitness - .value forces you to be aware of what you are doing

1. Convenience

The composition api is proposed to provide a way to group code with accordance to their feature in the component instead of their function in Vue. The options api groups code into data, computed, methods, lifecycles etc. This make it almost impossible to group code by feature. See the following image:

Consider the following examples:

const state = reactive({
  count: 0,
  errorMessage: null,
})
setTimeout(() => state.count++, 1000)
watch(state.count, count => {
  if (count > 10) {
    state.errorMessage = 'Larger than 10.'
  }
})
Enter fullscreen mode Exit fullscreen mode

If we use reactive() to store multiple properties. It is easy to fall back into the trap of grouping things by functions, not feature. You will likely be jumping around the code base to modify that reactive object. This makes the development process less smooth.

const count = ref(0)
setTimeout(() => count.value++, 1000)

const errorMessage = ref(null)
watch(count, count => {
  if (count > 10) {
    errorMessage.value = 'Larger than 10.'
  }
})
Enter fullscreen mode Exit fullscreen mode

On the other hand, ref() allow us to introduce new variables on the fly. From the example above, I only introduce variables as I need them. This makes the development process much smoother and intuitive.

2. Flexibility

I initially thought the sole purpose of ref() was to enable primitive values to be reactive. But it can become extremely handy too when using ref() with objects.

Consider:

const blogPosts = ref([])
blogPosts.value = await fetchBlogPosts()
Enter fullscreen mode Exit fullscreen mode

If we wish to do the same with reactive, we need to mutate the array instead.

const blogPosts = reactive([])
for (const post of (await fetchBlogPosts())) {
  blogPosts.push(post)
}
Enter fullscreen mode Exit fullscreen mode

or with our "beloved" Array.prototype.splice()

const blogPosts = reactive([])
blogPosts.splice(0, 0, ...(await fetchBlogPosts()))
Enter fullscreen mode Exit fullscreen mode

As illustrated, ref() is simpler to work with in this case as you can just replace the whole array with a new one. If that doesn't convince you, imagine if the blogPosts needs to be paginated:

watch(page, page => {
  // remove everything from `blogPosts`
  while (blogPosts.length > 0) {
    blogPosts.pop()
  }

  // add everything from new page
  for (const post of (await fetchBlogPostsOnPage(page))) {
    blogPosts.push(post)
  }
})
Enter fullscreen mode Exit fullscreen mode

or with our best friend splice

watch(page, page => {
  blogPosts.splice(0, blogPosts.length, ...(await fetchBlogPostsOnPage(page)))
})
Enter fullscreen mode Exit fullscreen mode

But if we use ref()

watch(page, page => {
  blogPosts.value = await fetchBlogPostsOnPage(page)
})
Enter fullscreen mode Exit fullscreen mode

It is much flexible to work with.

3. Explicitness

reactive() returns an object that we will interact with the same we interact with other non-reactive object. This is cool, but can become confusing in practise if we have deal with other non-reactive objects.

watch(() => {
  if (human.name === 'Jason') {
    if (!partner.age) {
      partner.age = 30 
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

We cannot really tell if human or partner is reactive. But if we ditch using reactive() and consistently utilise ref(), we won't have the same problem.

.value may seem wordy at first; but it helps reminding us that we are dealing with reactivity.

watch(() => {
  if (human.value.name === 'Jason') {
    if (!partner.age) {
      partner.age = 30 
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

It becomes obvious now that human is reactive but not partner.

Conclusion

The above observations and opinions are totally tentative. What do you think? Do you agree ref() is going to dominate in Vue 3? Or do you think reactive() will be preferred?

Let me know in the comments! I would love to hear more thoughts!

Top comments (27)

Collapse
 
ushironoko profile image
ushironoko

In the option api, each option is self-evident at the top level, making it easy to track reactive data consistently in any code. (This is the biggest advantage of option api)
Since all new APIs are wrapped in setup and reactive data is ejected from the extracted functions, it is not obvious which API the data comes from. By considering .value as a prefix, tracking reactive data can be made somewhat easier.

Collapse
 
smolinari profile image
Scott Molinari

This is why the best practice of using "useXXX" has been suggested. Any object called "useXXX" is considered reactive.

Scott

Collapse
 
chrismnicola profile image
Chris Nicola

I guess I have a few questions about this.

1) Why can't you use reactive to declare multiple variables as you are doing with ref and computed. I'm not sure I see why that is an advantage unless I misunderstand the usage.

2) Why, in your blog post example did you chose to declare a reactive array directly instead of reactive({ blogPosts: [] }) which would have made more sense and have largely the equivalent behaviour to using ref.

The API RFC document seems to explain fairly well where reactive and ref should and should not be used. I don't really see where reactive could really be dangerous outside of simply not understanding how it will behave relative to ref and computed.

vue-composition-api-rfc.netlify.co...

Collapse
 
denisinvader profile image
Mikhail Panichev

Nice article, thank you!

To be honest, I”m a little disappointed in the new reactivity system. I think if it’s immutable, the final code is more strict, predictable and cleaner. Especially in case of junior devs. I mean, deep object reactivity is unnecessary because if you have something like users[index].info.cats.push(newCat), you more likely have spaghetti code and should restructure this component.

By the way, will deep object mutation trigger updates only in specific child component?

Collapse
 
ycmjason profile image
YCM Jason

Why would .push more likely to introduce spaghetti code?

Will deep object mutation trigger updates only in specific child component?

Yes.

A component will only re-render if the dependencies of the render function are changed. So if a child component do not depend on the "deep object mutation", then it should not be re-rendered.

Collapse
 
chadonsom profile image
ChaDonSom

I've started using ref for the same reasons.

Ref is easier & cleaner.

I do wish we could do without .value, but every time I use reactive it comes back to bite me, so I'd much rather explicitly write .value than have to figure out the implicit.

Collapse
 
chibiblasphem profile image
Debove Christopher

Yeah I did have the same problem when I wanted to use a computed on a reactive array

// This computed won't update
const todos = reactive([])
const uncompletedCount = computed(() => todos.filter(t => !t.completed).length)

// Updates correctly the computed
const todos = ref([])
const uncompletedCount = computed(() => todos.value.filter(t => !t.completed).length)
Collapse
 
kwiat1990 profile image
Mateusz Kwiatkowski • Edited

Does anyone knows how to make this example work? It seems that computed with reactive data doesn't updated at any but why is so? (Strange enough I can see in Vue DevTools, that sometimes the value for this reactive computed get somehow updated, but as soons as I open DevTools it won't anymore).

It will only work if inside reactive we define an object because, as I understand it, reactive expectes some properties to work with,. Therefore there must be an object passed. I think Vue should handle this case much better and output some sort of an error or a warning to the console.

For example the code below will update the value as intended:

const todos = reactive({
  state: []
});
const uncompletedCount = computed(
  () => todosReactive.state.filter(t => !t.completed).length
);
Thread Thread
 
darrylnoakes profile image
Darryl Noakes

An array is an Object.

Collapse
 
sylvainpolletvillard profile image
Sylvain Pollet-Villard

Neither reactive nor ref feel like a good solution to me. We know that the options API is very approchable by beginners, which makes Vue successful, so as a consequence the Vue team had to deal with a lot of beginners mistakes ; for the options API, it was often a misunderstanding of how the this keyword works. So this has been flagged as a footgun and is avoided as much as possible in Vue 3 RFCs.

Yet I feel like using this in setup would have solve so many trouble, while being much closer to the remaining options API:

this.count = 0
setTimeout(() => this.count++, 1000)

this.errorMessage = null
watch(count, count => {
  if (count > 10) {
    this.errorMessage= 'Larger than 10.'
  }
})
Enter fullscreen mode Exit fullscreen mode
  • it works exactly like any other component option
  • no need to return explicitely the bindings, which is something I often forget when using Composition API
  • no trap like forgetting .value or destructuring reactive objects ; the only trap is when the context changes, but:
    • it's a language issue, not a Vue issue. It's something the developer will have to learn in JS at some point anyway
    • arrows functions are awesome, we use them anywhere and they don't rebind this

I believe the difficulty in composing logic while keeping the same execution context is overstated, nothing that can be fixed with a simple .call(this). And again, this is a general JavaScript issue, nothing specific to Vue. Devs will meet the exact same issue on the logic/services parts of their app, if they mix OOP/FP style, which seems to be a current trend.

Collapse
 
voluntadpear profile image
Guillermo Peralta Scura

Another great benefit of refs is that if you return an object where each property is a ref from a composition function, you can destructure it safely.

Collapse
 
wormss profile image
WORMSS

Are the return types from ref().value recursive read-only?
Or are they still mutable? Are they reactive if so?

Collapse
 
voluntadpear profile image
Guillermo Peralta Scura

ref.value is mutable, and if the inner value of your ref is an object it is deeply reactive. E.g.: myObjectRef.value.bar.foo = "5" will trigger any watcher on myObjectRef

Thread Thread
 
ycmjason profile image
YCM Jason

If it is a computed ref, then I believe it will be recursively read-only.

Collapse
 
smolinari profile image
Scott Molinari

There is a helper function for destructuring a reactive object called toRefs(). So, in the end, you can just use reactive objects and destructure them with toRefs().

Scott

Collapse
 
forceuser profile image
Vitaly Dub

Have you considered this way of working with "reactive"? In my opinion it's better than use .value but have all the best of "ref" kind of declaration

<template>
    <div id="app">
        <input v-model="val" />
        {{ $val }}
    </div>
</template>

<script>
import {reactive, computed} from "vue";

export default {
    setup () {
        const state = reactive({});

        state.val = "abc";
        state.$val = computed(() => `--${state.val}--`);

        state.arrayVal = []; // array is reactive and you can reassign it

        return state;
    },
};
</script>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ycmjason profile image
YCM Jason

Yes. I actually proposed that to Evan before too. But after actually using the composition API, I was surprised to find ref much more natural to use. Having said that, using a state object is totally valid pattern tho!

Collapse
 
andykras profile image
Andrey Krasnov

this should work the same way as ref()

const blog = reactive({posts: []})
blog.posts = await fetchBlogPosts()
Collapse
 
ycmjason profile image
YCM Jason

Exactly! So just use ref!

 
kjetilhaaheim profile image
KjetilHaaheim

I appreciate your input, and Vue is definitely not holy in my eyes. It is a framework amongst many, your comment was simply off topic when the discussion is clearly about Vue3 Composition API, and not a debate about which X is better than Y. The agressivity in your reply, you can take elsewhere.

Collapse
 
httpjunkie profile image
Eric Bishard

This comment is a bit out of place..