Sarah Ting

Scraps: Vue 3 & composition API notes

⚖️ Composition vs Options Api

  • Vue is moving away from the “Options API” from Vue 2, where concerns and namespacing are spread across data(), methods, watch , props, etc.
  • Vue3 introduces the Composition API, and the concept of a single setup that determines what data/functions are returned to the template.

An example of Options API vs Composition API:

Options API

<template>
    <div @click="greet()">
      Current user: {{username}}
	</div>
</template>

<script>
export default {
  data() {
    return {
      username: 'Sarah',
    },
  },
  methods: {
    greet() {
      console.log(`Hello ${this.username}`);
    },
  },
  mounted() {
    this.greet();
  }
}
</script>

Composition API

<template>
  <div @click="greet()">
    Current user: {{username}}
	</div>
</template>

<script>
import { ref, onMounted } from 'vue';
export default {
  setup() {
    const username = ref('Sarah');
    const greet = () => console.log(`Hello ${username.value}`);

    onMounted(() => greet());

    return {username, greet};
  }
}
</script>

Composition API with <script setup>

<template>
  <div @click="greet()">
    Current user: {{username}}
	</div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const username = ref('Sarah');
const greet = () => console.log(`Hello ${username.value}`);

onMounted(() => greet());
</script>

Benefits of Composition API

  • Can group code by logical concern rather than what it does (eg. data, methods, watchers)
  • Can choose which parts of your code to share with your template or make accessible to other components
  • Improved ability to cleanly share logic:
    • With options API, the only way to share logic was Mixins and Renderless Components
    • With composition, it’s easier to reuse logic in multiple components by creating bits of code called composables, or composable functions (more on this further down)

📊 Reactive data

  • With the Options API, the data() function was intended to be used for both reactive and non-reactive variables
  • With the Composition API, we explicitly define which variables are reactive

Using ref() for primitives

Options API

<template>
  <div>Hi {{username}} (#{{userId}})</div>
</template>
<script>
export default {
  data() {
    return {
      // if either of these change, we expect the template to rerender
      username: 'Sarah',
      userId: 1,
    },
  },
  methods: { 
    setUsername(username) {
      this.username = username;
    },
  }
}
</script>

Composition API

<template>
  <div>Hi {{username}} (#{{userId}})</div>
</template>
<script>
import {ref} from 'vue';
export default {
  setup() {
    // read-only (even if it does change, template will not rerender)
    const userId = 1;

    // reactive (template will rerender when this changes)
    const username = ref('Sarah');

    return {
      userId, 
      username, 

      // 1. methods can just be passed back together with data 
      // 2. note that we're changing username.value and not username --
      // this is because username is wrapped in a Ref
      setUsername: (username) => username.value = username,
    }
  }
}
</script>

Using reactive() for objects

Options API

<template>
  <div>Hi {{user.username}} (#{{user.id}})</div>
</template>
<script>
export default {
  data() {
    return {
      user: {username: 'Sarah', id: 1},
    },
  },
  methods: { 
    setUsername(username) {
      this.user.username = username;
    },
  },
}
</script>

Composition API

<template>
  <div>Hi {{user.username}} (#{{user.id}})</div>
</template>
<script>
import {reactive} from 'vue';
export default {
  setup() {
    const user = reactive({username: 'Sarah', id: 1});

    return {
      user, 

      // note that we do not need "value" as a key here
      setUsername: (username) => user.username = username,
    };
  },
}
</script>

Difference between ref() vs reactive()

  • ref() is usually used for primitives and reactive() is usually used for objects
  • Note that ref() actually is just a wrapper for a reactive() call
  • Destructuring a reactive() object will make the individual fields non-reactive; in this case, you’ll want to turn each of the reactive children into refs using the toRefs() helper:
import {toRefs, reactive} from 'vue';

//...

setup() {
    const user = reactive({name: 'Sarah', id: 1});
    return {...toRefs(user)}
}

😵‍💫 Template refs

Don’t get confused; in Vue 3, template “ref” elements are conceptually the same as the reactive ref() — you use template “ref” elements by creating a ref() with the same variable name

<template>
  <div>
    <input ref="singleInput">
    <input ref="multiInput">
    <input ref="multiInput">
    <input ref="multiInput">
  </div>
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
  setup() {
    // the name must match template ref value; ref elements will always start as "null"
    const singleInput = ref(null);

    // multiple inputs will give an array 
    const multiInput = ref([]);

    // the ref element doesn't exist until mount!! this can't be called in setup()!
    onMounted(() => {
      singleInput.value.focus();
    })

    // must be passed down!!
    return { singleInput, multiInput }
  }
}
</script>

💻 Computed variables

Options API


<template>
  <div>
    {{hours}} hours is {{minutes}} minutes
  </div>
</template>

<script>
export default {
  data() {
    return { hours: 1 };
  }
  computed {
    minutes() {
      return this.hours * 60;
    },
  }
}
</script>

Composition API read-only computed

<template>
  <div>
    {{hours}} hours is {{minutes}} minutes
  </div>
</template>

<script>
import {computed} from 'vue';

export default {
  setup() {
    const hours = ref(1);
    const minutes = computed(() => hours.value * 60);

    return {hours, minutes};
  }
}
</script>

Composition API read-write computed

<script>
import {computed} from 'vue';

export default {
  setup() {
    const hours = ref(1);
    const minutes = computed({
      get: () => hours.value * 60,
      set: (minutes) => hours.value = minutes / 60,
     });

    return {hours, minutes};
  }
}
</script>

👀 Watchers

Using watch()

Options API

<template>
  <div>
    Logged in: {{ username }}
  </div>
</template>

<script>
export default {
  data() {
    return { username: 'Sarah' };
  },
  watch: {
    username(newVal, oldVal) {
      console.log(`User change: ${oldVal} -> ${newVal}`); 
    }
  },
}
</script>

Composition API

<template>
  <div>
    Logged in: {{ username }}
  </div>
</template>

<script>
import {watch} from 'vue';

export default {
  setup() {
    const username = ref('Sarah');

    // first arg: (1) ref/reactive OR (2) getter fn OR (3) array of ref/reactive/getter
    watch(username, (newVal, oldVal) => console.log(`User change: ${oldVal} -> ${newVal}`));

    return {username};
  }
}
</script>

Composition API watching multiple refs

setup() {
  const username = ref('Sarah');
  const userId = ref(1);

  watch(
    [username, userId], 
    ([newUsername, oldUsername], [newUserId, oldUserId]) => {/* ... */}
  );

  return {username, userId};
}

Using watchEffect()

  • An alternative to watch()
setup() {
  const username = ref('Sarah');
  const userId = ref(1);

  // this triggers whenever username or userId change
  watchEffect(() => console.log(`Logged in user: ${username.value}, id: ${userId.value}`)));

  return {username, userId};
}

Difference between watchEffect() and watch()

  • watchEffect() :
    • Will automatically watch any ref/reactive/getter dependencies
    • Will ALWAYS trigger immediately (and then on every change thereafter)
    • Does not return the old value of the ref/reactive/getter dependencies
  • watch():
    • Lets you explicitly specify which ref/reactive/getters the function is dependent on
    • By DEFAULT will not trigger immediately (will only trigger on changes), but can be configured to trigger immediately also
    • Does return both the new and old value of the ref/reactive/getter dependencies
    • Seems more configurable & flexible

♻️ Lifecycle hooks

Options API

<script>
export default {
  mounted() {
    console.log('mounted');
  },
}
</script>

Composition API

<script>
import {onMounted} from 'vue';

export default {
  setup() {
    onMounted(() => console.log('mounted'))
  }
}
</script>

Nothing surprising here! Can read more about available hooks in the Vue JS documentation:


🍬 <script setup>

  • imo this is very ugly but vue strongly recommends use of script setup
  • <script setup> lets you cut out the setup() function —
    • everything inside the <script setup></script> tags will be treated as if it’s inside the setup() function
    • all top-level variables are exposed to the template
  • idk if there’s a reason but every example i’ve seen that uses <script setup> also puts the script above the template instead of below
<script setup>
import { ref } from 'vue'

const greeting = 'Hello world!';
const count = ref(0);

function alert() {
  console.log(greeting)
}
</script>

<template>
  <div>
    <a href="#" @click="alert">{{greeting}}</a>
    <span @click="count++">{{count}}</span>
  </div>
</template>

Warnings

  • Remember that<script setup> will become your setup() — accordingly it’s not safe to put top-level code here that you would normally put outside of your setup() definition — this code will be run every single time the component gets setup/initialized

📝 Props, components, etc

These are mostly unchanged!

Options API

<template>
  <my-component>{{myProp}}</my-component>
</template>

<script>
import MyComponent from './components/MyComponent';

export default {
  components: {MyComponent},
  props: ['myProp']
}
</script>

Composition API with setup()

<template>
  <my-component>{{myProp}}</my-component>
</template>

<script>
import MyComponent from './components/MyComponent';

export default {
  components: {MyComponent},
  props: ['myProp'],

  // props are passed into setup if needed
  // but will automatically go down to the template regardless
  setup(props) {
    console.log(props.myProp);
  }
}
</script>

<script setup> uses defineProps()

<template>
  <my-component>{{myProp}}</my-component>
</template>

<script setup>
// this will automatically be exposed to the template
import MyComponent from './components/MyComponent';
import {defineProps} from 'vue';

// use defineProps for importing props
const {myProp} = defineProps(['myProp']);
console.log(myProp);
</script>

📦 Mixins vs Composables

Benefits of composables:

  • Can encapsulate data & functionality into a single component (better organization and partitioning of data and functionality; there is no ambiguity about where data or functionality is coming from)
  • Can have private data & methods

Mixin

/**********************************************************/
/* CounterMixin.js
/**********************************************************/
export default {
  data() {
    return { count: 0 };
  },
  computed: { 
    countPlusOne() {
      return this.count + 1;
    },
  },
  methods: {
    increment() {
      this.count ++;
    }
  },
}

/**********************************************************/
/* MyComponent.vue
/**********************************************************/

import CounterMixin from '../mixins/CounterMixin';
import SarahsCounterMixin from '../mixins/SarahsCounterMixin';
import AnotherCounterMixin from '../mixins/AnotherCounterMixin';

export default {
  mixins: [CounterMixin, SarahsCounterMixin, AnotherCounterMixin],
  created() {
    console.log(this.count); // which count is this referring to? ambiguous
    console.log(this.countPlusOne); // sussy
    this.increment(); // very sussy
  },
}

Composable

/**********************************************************/
/* useCounter.js
/**********************************************************/
import {ref, computed, readonly} from 'vue';

// composables can support global data!! 
// this variable is initialized outside of the composable function, so every instance of useCounter will share the same count
// move this into the function if you don’t want this behaviour (count will start at 0 for every component)
const count = ref(0); 

// use of readonly here means that even though we pass count as a Ref, it can’t be edited without calling increment
export default () => {
  const increment = () => count.value ++;
  const countPlusOne = computed(() => count.value + 1);
  return {count: readonly(count), increment, countPlusOne}
}

/**********************************************************/
/* MyComponent.vue
/**********************************************************/

import {useCounter} from '../composables/useCounter';
import {useSarahsCounter} from '../composables/useSarahsCounter';
import {useAnotherCounter} from '../composables/useAnotherCounter';

export default {
  setup() {
    const {count, increment, countPlusOne} = useCounter();
    const {count: sarahsCount, increment: sarahsIncrement, countPlusOne: sarahsCountPlusOne} = useSarahsCounter();

		console.log(count.value);
    console.log(countPlusOne.value);
    increment();

    return {count, countPlusOne, increment};
  }
}

🏪 Vuex


<template>
  <h1>{{title}}</h1>
</template>

<script setup>
import {useStore} from 'vuex';
import {computed} from 'vue';

const store = useStore(); // useStore() call should be inside setup and not inside getters/computed/hooks etc

const title = computed(() => `Hello world ${store.state.username}`);
const setUsername = (username) => store.commit('setUsername', username);
</script>

🚀 Using SWRV (SWR Vue)

https://github.com/Kong/swrv

  • SWR stands for “stale-while-revalidate”
    1. Returns any cached (”stale”) data first to the UI
    2. Send a fetch request for new data (revalidate)
    3. Returns up to date data after it comes back
  • Benefits:
    • Makes UI feel faster
    • Handles automatic revalidation (when the tab is refocused, on interval, or on reconnect)
    • Supports pagination & infinite loading (provides a useSwrInfinite hook)
    • If an error is returned on revalidation, the cached data will remain in cache and be served

No fetcher (default behaviour)

  • This will automatically call the API using fetch
<template>
  <div>
    <div v-if="error">failed to load</div>
    <div v-if="!data">loading...</div>
    <div v-else>hello {{ data.name }}</div>
  </div>
</template>

<script>
import useSWRV from 'swrv'

export default {
  name: 'Profile',

  setup() {
    const { data, error } = useSWRV('/api/user')
    return { data, error }
  },
}
</script>

Null fetcher

  • This will never revalidate and will only cache
  • Use case, eg: you have two components which are using the same cache key; if one component is revalidating, you don’t need to revalidate for the second component
setup() {
  const { data, error } = useSWRV('/api/user', null)
  return { data, error }
}

Explicit fetcher

  • Will use the provided Promise for revalidation
  • To be honest, I don’t know the use case for this lol
setup() {
  const { data, error } = useSWRV('/api/user', () => axios.get('/api/user/'))
  return { data, error }
}

Dependent fetching

This is pretty cool...

setup() {
  const { data: user } = useSWRV('/api/user')
  const { data: projects } = useSWRV(() => user.value && `/api/user/${user.value.id}/projects`)
  // the fetcher will not trigger if the cache key is falsy, but will watch `user` for changes since it's reactive

  return { user, projects }
},

📔 Reading