DEV Community

Attila Szeremi⚡
Attila Szeremi⚡

Posted on

Vue Multiple Logic Topics, but with the Object API

There's been a bit of debate recently about the upcoming VueJS 3 Function API. It brings about several benefits, like being able to to make sharing code behavior easier for library authors, and better TypeScript support.

There are various downsides of implementing this of course, out of which added complexity, and inelegant syntax according to some.

I don't want to talk about all the upsides and downsides of this new Function API though.

I only want to talk about one supposed upside that Function API would bring, but that I claim to be completely achievable simply using the Object API available in VueJS 2:

Multiple Logic Topics

According to the Function API doc, though its example is simplified, it brings up the fact that large VueJS components in large projects can have multiple topics (i.e. domains) of logic happening, but that it's hard to tell which properties defined in data, methods, computed etc. belong to which topic.

Here is the example of mixing topics about both mouse movement tracking and data fetching in VueJS 2. Note that the example is simplified; it's potentially a bigger issue in larger code.

<template>
  <div>
    <template v-if="isLoading">Loading...</template>
    <template v-else>
      <h3>{{ post.title }}</h3>
      <p>{{ post.body }}</p>
    </template>
    <div>Mouse is at {{ x }}, {{ y }}</div>
  </div>
</template>
import { fetchPost } from './api'

export default {
  props: {
    id: Number
  },
  data() {
    return {
      isLoading: true,
      post: null,
      x: 0,
      y: 0
    }
  },
  mounted() {
    this.fetchPost()
    window.addEventListener('mousemove', this.updateMouse)
  },
  watch: {
    id: 'fetchPost'
  },
  destroyed() {
    window.removeEventListener('mousemove', this.updateMouse)
  },
  methods: {
    async fetchPost() {
      this.isLoading = true
      this.post = await fetchPost(this.id)
      this.isLoading = false
    },
    updateMouse(e) {
      this.x = e.pageX
      this.y = e.pageY
    }
  }
}

Here is the VueJS 3 proposed cleaner separation of topics with the help of the Function API:

import { fetchPost, value, watch, onMounted, onUnmounted } from './api'

function useFetch(props) {
  const isLoading = value(true)
  const post = value(null)

  watch(() => props.id, async (id) => {
    isLoading.value = true
    post.value = await fetchPost(id)
    isLoading.value = false
  })

  return {
    isLoading,
    post
  }
}

function useMouse() {
  const x = value(0)
  const y = value(0)
  const update = e => {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}

export default {
  setup(props) {
    return {
      ...useFetch(props),
      ...useMouse()
    }
  }
}

Indeed one can argue that the Function API is great for giving you an opportunity to group logic belonging to the same topic together and package it under reusable functions.

However, it's completely possible to effectively do the same thing with VueJS 2's old-fashioned Object API. It would involve writing the topic code to variables before your VueJS component object definition:

import { fetchPost } from "./api"

const fetchData = {
  data: {
    isLoading: true,
    post: null
  },
  mounted() {
    this.fetchPost()
  },
  watch: {
    id: "fetchPost"
  },
  methods: {
    async fetchPost() {
      this.isLoading = true
      this.post = await fetchPost(this.id)
      this.isLoading = false
    }
  }
}
const mouseData = {
  data: {
    x: 0,
    y: 0
  },
  mounted() {
    window.addEventListener("mousemove", this.updateMouse)
  },
  destroyed() {
    window.removeEventListener("mousemove", this.updateMouse)
  },
  methods: {
    updateMouse(e) {
      this.x = e.pageX
      this.y = e.pageY
    }
  }
}
export default {
  props: {
    id: Number
  },
  data() {
    return {
      ...fetchData.data,
      ...mouseData.data
    }
  },
  mounted() {
    fetchData.mounted.call(this)
    mouseData.mounted.call(this)
  },
  watch: {
    ...fetchData.watch
  },
  destroyed() {
    mouseData.destroyed.call(this)
  },
  methods: {
    ...fetchData.methods,
    ...mouseData.methods
  }
}

As you can see, the topics have been separated into fetchData and mouseData. Then we explicitly do object spreading in each property of the Object API for each topic that has it.

The object spreading may seem strange at first, but if you think about it, it's used commonly in Vuex's mapXXX() functions.

But there is a bit of code repetition here. And one may forget to spread on something from one of the topic's properties, or include calling the lifetime methods, or forget to bind this. I wonder if there's a way we could simplify this code and remove a lot of boilerplate...

import { fetchPost } from "./api"

const fetchData = {
  data: {
    isLoading: true,
    post: null
  },
  mounted() {
    this.fetchPost()
  },
  watch: {
    id: "fetchPost"
  },
  methods: {
    async fetchPost() {
      this.isLoading = true
      this.post = await fetchPost(this.id)
      this.isLoading = false
    }
  }
}
const mouseData = {
  data: {
    x: 0,
    y: 0
  },
  mounted() {
    window.addEventListener("mousemove", this.updateMouse)
  },
  destroyed() {
    window.removeEventListener("mousemove", this.updateMouse)
  },
  methods: {
    updateMouse(e) {
      this.x = e.pageX
      this.y = e.pageY
    }
  }
}
export default {
  props: {
    id: Number
  },
  mixins: [fetchData, mouseData]
}

Surprise surprise, this is precisely what mixins do :D.

In case you haven't thought of this before, you can define mixins and use them in the same file too. In which case the common argument is not so strong anymore against mixins about property clashing, and that you don't know which file the properties come from. But one could argue that this code both separates topics well and is pretty clean an simple too.

Closing thoughts

With this of course you can't share behavior between "topics" equally well as you could with the Function API (think useEffect() with React Hooks), probably. Also there are other benefits to the Function API as well.

The real purpose of my article was solely to demonstrate that the one argument about the new Function API bringing about the ability to separate topics is false, because you can do the same with the existing Object API.

Discussion (2)

Collapse
reegodev profile image
Matteo Rigon

What if both mixins come from a third party library and both declare the same reactive property isLoading?

Collapse
amcsi profile image
Attila Szeremi⚡ Author • Edited on

Then you're screwed, I believe. I'm not claiming that you can do everything the Function API can do with the Object API.