loading...

Thoughts on building composition-api functions

pikax profile image Carlos Rodrigues Updated on ・5 min read

After vue-next became publicly available, inspired by LinusBorg composition-api-demos, I started building a utility composition-api library vue-composable with a goal of learning and understanding the composition-api.

Typescript

Because of my background on C#, I'm really keen to have intellisense, I always went the extra mile to get types on my vue apps, even when it required heavily modify and adapt vuex typings or other caveats of using typescript with vue.

I must admit using typescript within setup() has been really pleasant, it feels like plain typescript (similar to react in a way), without sleight of hands.

composition-api

IMHO composition-api shines when composing multiple functions to get the desire result.

Let's try implementing SWAPI composable:

import { usePagination, useFetch, wrap } from "vue-composable";
import { ref, watch, isRef } from "@vue/composition-api";

type SWAPI_RESOURCE =
  | "planets"
  | "spaceships"
  | "vehicles"
  | "people"
  | "films"
  | "Species";

interface SWAPIList<T = any> {
  count: number;
  next: string;
  previous: string;
  results: Array<T>;
}

function useSWAPI<T = any>(r: SWAPI_RESOURCE) {
  const resource = wrap(r);
  const ENDPOINT = `https://swapi.co/api/`;

  const items = ref<T[]>([]);
  const { json, loading, exec, status } = useFetch<SWAPIList>();

  const pagination = usePagination({
    currentPage: 1,
    pageSize: 10, // default size
    total: 0
  });

  watch(
    json,
    json => {
      if (json) {
        pagination.total.value = json.count;
        items.value = json.results;
      } else {
        pagination.total.value = 0;
        items.value = [];
      }
    },
    {
      lazy: true
    }
  );

  watch([pagination.currentPage, resource], () => {
    exec(`${ENDPOINT}${resource.value}/?page=` + pagination.currentPage.value);
  });

  return {
    ...pagination,
    exec,
    items,
    loading,
    status
  };
}

// usage
setup(){
  return useSWAPI('people');
}

We can improve the return types from the request based on SWAPI_RESOURCE.

In this example we use two composables usePagination and useFetch

  • usePagination allows manipulating pages based on items, it's generic enough to allow to adapt any pagination implementation.
  • useFetch just a fetch wrapper

Re-usability

You might be thinking "Isn't that what mixins are used for?" and you be correct, but using mixins you need to be careful with naming collisions, handling variable names, methods, etc.

Using composition-api becomes trivial to expose multiple api calls on the setup:

setup(){
  const people = useSWAPI('people');
  const planets = useSWAPI('planets');

  return {
    people,
    planets
  }
}

Ref vs Reactive

I recommend having a look on this Thought on Vue 3 Composition API - reactive() considered harmful

When building vue-composable 98% of the cases I will return a object with ref, the reason being it allows you to deconstruct your object and vue will unwrap it on the render.

One common practice I use on my composables is accept both Ref<T>|T, this allows the flow in the setup() to be much cleaner (without .value everywhere) and also allowing the composable to watch changes on the argument.

Template unwrapping

One of the arguments of using ref is the auto-unwrapping on the template(no need to use .value in the render), but the commit refactor: remove implicit reactive() call on renderContext, disables the auto unwrapping of the object (more info), making the usage of ref a bit more verbose

export default {
  // before 
  template: `<div> {{ awesomeObject.items }} {{ awesomeObject.selected }} </div>`,
  // after
  template:  `<div> {{ awesomeObject.items.value }} {{ awesomeObject.selected.value }} </div>`,
  // after with auto unwrap
  template:  `<div> {{ autoUnwrap.items }} {{ autoUnwrap.selected }} </div>`,
  setup() {
    const awesomeObject = {
      items: ref([]),
      selected: ref({}),
    };

    return {
      awesomeObject,
      // auto unwrapping, it need to be a ref, cannot return plain object with nested ref
      autoUnwrap: ref(awesomeObject) // or reactive(awesomeObject)
    };
  }
};

This is a breaking change and as far as I know, the @vue/composition-api it not updated yet.

This change makes the usage of ref less appealing, but not sure how in the real world environment what changes it will make.

Not everything needs to be ref or reactive

This might be a bit controversial, I don't believe your use* should always return ref, when you are returning something you know it will not change, you might be better off not wrapping it on a ref/reactive, eg:

export function useOnline() {
  const supported = "onLine" in navigator;

  // not sure how to test this :/
  if (!supported) {
    online = ref(false);
  }

  if (!online) {
    online = ref(navigator.onLine);
    // ... listen for changes
  }

  return {
    supported,
    online
  };
}

supported won't change, so the usage of a ref is not necessary, I don't think consistency is a good argument in this particular case.

using reactive(state) and then return toRefs()

I've seen code that uses an reactive state and then return toRefs(state).

I like how clean this is, you just need to understand why you need to return toRefs(state) and that's basically the complexity of this approach.

const state = reactive({
  supported: "onLine" in navigator,
  online: navigator.onLine
})
window.addEventListener("online", () => state.online = true));

return toRefs(state);

Although as a library creator, having to call toRefs will have a theoretically more objects created (just an opinion, I might be wrong, you can prove me wrong), thus more GC work. Asides from that, I think is a pretty neat way to overcome .value 👍

Moving to composition-api

IMO you don't need to port your object-api code to composition-api, I would go as far as don't recommend doing it without a good reason.

object-api only has a few issues when dealing with huge components, you can even make an argument that you should refactor your component better, I would only use composition-api if maintaining your component if getting out-of-hand and composition-api would allow to make the component easier to maintain.

You can use composition-api return values on your object-api

When using @vue/composition-api plugin, it is built on top of the object-api and when using vue-next the options-api is built using composition-api, making it trivial to share functionality between them.

For example, our useSWAPI example, if you want to use it on your vue component without using setup(), you can use:

export default {
  data(){ 
   return reactive(useSWAPI('people')), 
   // you need to use `reactive` or `ref` to unwrap the object,
   // otherwise you need to use `.value` on the template
  }
}

Final thoughts

I really like the extensibility of the composition-api, I'm looking forward for what the community will build once vue3 is out!

I recommend being open-minded and use the right tool for the job, some components don't required extra complexity or you don't want to migrate your huge project to composition-api, my answer to that is: You don't need it.

You can take advantage of the community libraries for composition-api within your object-api component, to be fair that's one thing I like VueJS it provides an standard way to do things but also giving you the tool to allow modifying and tweak it for your needs.

I know the composition-api was quite controversial in the beginning, let me know if you still think is unnecessary or if you are interested in learning or if you're using it already or what's your thoughts on this matter?

You can check some of my choices and implementations: https://github.com/pikax/vue-composable

EDIT

2020-03-01

Striking the text that mentions the commit to prevent auto-unwrapping on template, because the auto-unwrapping was re-added
refactor(runtime-core): revert setup() result reactive conversion

Posted on by:

pikax profile

Carlos Rodrigues

@pikax

Passionate about new tech. VueJS lover <3

Discussion

markdown guide