@vueschool_io

vueschool.io

Welcome

πŸ‘‹

Vue 3 and the Composition API

πŸ“š You will learn

  • Essential Composition API syntax and functions

  • Script setup
  • When and how you should use the Composition API
  • Using composition functions for logic re-use
  • Organizing code by feature
  • Using 3rd party composables
  • Lifecycle Hooks with the Composition API

With the experience and codes from these workshops you will be able to use the Composition API in your apps right away!

Setup Exercise Project Locally

All exercises can be completed on StackBlitz as well

⏰ 10 mins

What is the Composition API?

What is the Composition API?

What do you think?

Answer in the chat

Vue 2 used the Options API

new Vue({
  data(){
    return {
      loading: false,
      count: 0,
      user: {}
    }
  },
  computed: {
    double () { return this.count * 2 },
    fullname () {/* ... */}
  },
  methods: {
    increment () { this.count++ },
    fetchUser () {/* ... */}
  }
})

Vue 3 introduces the Composition API

const loading = ref(false)
const count = ref(0)
const user = reactive({})
new Vue({
  data(){
    return {
      loading: false,
      count: 0,
      user: {}
    }
  },
  computed: {
    double () { return this.count * 2 },
    fullname () {/* ... */}
  },
  methods: {
    increment () { this.count++ },
    fetchUser () {/* ... */}
  }
})

Define reactive data with

ref

or

reactive

const loading = ref(false)
const count = ref(0)
const user = reactive({})
new Vue({
  data(){
    return {
      loading: false,
      count: 0,
      user: {}
    }
  },
  computed: {
    double () { return this.count * 2 },
    fullname () {/* ... */}
  },
  methods: {
    increment () { this.count++ },
    fetchUser () {/* ... */}
  }
})

Should I use

ref

or

reactive

?

For now, just use ref.

More on this later

const double = computed(()=> count.value * 2)
const fullname = computed(()=>({ /* ... */ }))
new Vue({
  data(){
    return {
      loading: false,
      count: 0,
      user: {}
    }
  },
  computed: {
    double () { return this.count * 2 },
    fullname () {/* ... */}
  },
  methods: {
    increment () { this.count++ },
    fetchUser () {/* ... */}
  }
})

Define derived data with

computed

const increment = ()=> count.value++
function fetchUser(){ /* ... */ }
new Vue({
  data(){
    return {
      loading: false,
      count: 0,
      user: {}
    }
  },
  computed: {
    double () { return this.count * 2 },
    fullname () {/* ... */}
  },
  methods: {
    increment () { this.count++ },
    fetchUser () {/* ... */}
  }
})

Define methods as

functions

import { ref, computed, watch } from 'vue'

No longer passing options INTO Vue

Instead we're getting reactive functions OUT OF Vue

Can use reactivity anywhere

outside components

outside a Vue app

inside components

Example of using reactivity outside of a Vue app

Example of using reactivity inside of a Vue component

Questions?

πŸ™‹πŸΎβ€β™€οΈ

Exercises 1 & 2

πŸ‹οΈβ€β™€οΈ πŸ‹οΈ

<script setup>

So far, we've used the setup option without a build tool but the experience isn't great

Must return everything from the setup option

<script>
export default{
  setup(){
    const loading = ref(true);
    const products = ref([]);
    const numberOfProducts = computed(() => products.value.length);
    async function fetchProducts() { 
      //...
    }
    return { 
      loading, 
      products, 
      numberOfProducts, 
      fetchProducts
    };
  }
}
</script>

Plus there's a lot of unnecessary indentation

<script>
export default{
  setup(){
    const loading = ref(true);
    const products = ref([]);
    const numberOfProducts = computed(() => products.value.length);
    async function fetchProducts() { 
      //...
    }
    return { 
      loading, 
      products, 
      numberOfProducts, 
      fetchProducts
    };
  }
}
</script>

<script setup>

compile-time syntactic sugar for using Composition API

inside Single-File Components (SFCs)

<script setup>

  • must use a build tool like Vite or Webpack
  • Cannot use with Vue from a CDN
  • Composition API === use a build tool

How does script

setup work?

How does script

setup work?

<script>
export default{
  setup(){
    const loading = ref(true);
    const products = ref([]);
    const numberOfProducts = computed(() => products.value.length);
    async function fetchProducts() { 
      //...
    }
    return { 
      loading, 
      products, 
      numberOfProducts, 
      fetchProducts
    };
  }
}
</script>

How does script

setup work?

<script setup>
export default{
  setup(){
    const loading = ref(true);
    const products = ref([]);
    const numberOfProducts = computed(() => products.value.length);
    async function fetchProducts() { 
      //...
    }
    return { 
      loading, 
      products, 
      numberOfProducts, 
      fetchProducts
    };
  }
}
</script>

Add setup attribute to the script tag

How does script

setup work?

<script setup>
export default{
  setup(){
    const loading = ref(true);
    const products = ref([]);
    const numberOfProducts = computed(() => products.value.length);
    async function fetchProducts() { 
      //...
    }
    return { 
      loading, 
      products, 
      numberOfProducts, 
      fetchProducts
    };
  }
}
</script>

Remove the options object

How does script

setup work?

<script setup>

  setup(){
    const loading = ref(true);
    const products = ref([]);
    const numberOfProducts = computed(() => products.value.length);
    async function fetchProducts() { 
      //...
    }
    return { 
      loading, 
      products, 
      numberOfProducts, 
      fetchProducts
    };
  }

</script>

Remove the options object

How does script

setup work?

<script setup>

  setup(){
    const loading = ref(true);
    const products = ref([]);
    const numberOfProducts = computed(() => products.value.length);
    async function fetchProducts() { 
      //...
    }
    return { 
      loading, 
      products, 
      numberOfProducts, 
      fetchProducts
    };
  }

</script>

Remove the setup option

How does script

setup work?

<script setup>


    const loading = ref(true);
    const products = ref([]);
    const numberOfProducts = computed(() => products.value.length);
    async function fetchProducts() { 
      //...
    }
    return { 
      loading, 
      products, 
      numberOfProducts, 
      fetchProducts
    };


</script>

Remove the setup option

How does script

setup work?

<script setup>


    const loading = ref(true);
    const products = ref([]);
    const numberOfProducts = computed(() => products.value.length);
    async function fetchProducts() { 
      //...
    }
    return { 
      loading, 
      products, 
      numberOfProducts, 
      fetchProducts
    };


</script>

Remove all the returns

How does script

setup work?

<script setup>


    const loading = ref(true);
    const products = ref([]);
    const numberOfProducts = computed(() => products.value.length);
    async function fetchProducts() { 
      //...
    }








</script>

Remove all the returns

How does script

setup work?

<script setup>
    const loading = ref(true);
    const products = ref([]);
    const numberOfProducts = computed(() => products.value.length);
    async function fetchProducts() { 
      //...
    }
</script>

How does script

setup work?

<script setup>
const loading = ref(true);
const products = ref([]);
const numberOfProducts = computed(() => products.value.length);
async function fetchProducts() { 
  //...
}
</script>

Ahhh... that's nice!

More about

<script setup>

Convention = Script setup ABOVE template

<script setup>
 //...
</script>
<template> 
 //...  
</template>
<template> 
 //...  
</template>
<script>
export default{}
</script>

❌

βœ…

Import Components to Register them

<script>
import HelloWorld from "@/components/HelloWorld.vue";

export default {
  components: {
    HelloWorld,
  },
};
</script>
<script setup>
import HelloWorld from "@/components/HelloWorld.vue";
</script>

❌

βœ…

Define directives by prefixing with v

<script>
const autoFocus = {
  mounted: (el) => el.focus()
}

export default {
  directives: [ autoFocus ]
};
</script>
<script setup>
const vAutoFocus = {
  mounted: (el) => el.focus()
}
</script>

❌

βœ…

<template>
  <input v-auto-focus />
</template>

Define props

<script>
export default {
  props: {
    product: Object,
  },
  created(){
    console.log(this.product)
  }
};
</script>
<script setup>

const props = defineProps({
  product: Object,
});

console.log(props.product)
</script>

❌

βœ…

define with defineProps and access values on the return

Define emits

<script>
export default {
  emits: ['submit'],
  created(){
    this.$emit("submit")
  }
}
</script>
<script setup>
const emit = defineEmits(['submit'])

emit('submit')
</script>

❌

βœ…

define with defineEmits and the emit function is returned

Define Template Refs

<script>
  export default{
    onMounted(){
      this
        .$refs
        .input
        .focus()
    }
  }
</script>

<template>
  <input ref="input" />
</template>
<script setup>
import { ref, onMounted } from 'vue'

// declare a ref to hold the element reference
// the name must match template ref value
const input = ref(null)

onMounted(() => {
  input.value.focus()
})
</script>

<template>
  <input ref="input" />
</template>

❌

βœ…

Questions?

πŸ™‹πŸΎβ€β™€οΈ

Exercise 3

πŸ‹οΈβ€β™€οΈ πŸ‹οΈ

Recap

  • Vue Composition API is an alternate way of interacting with Vue's reactivity system
  • It can be used within components and outside of components
  • Reactive data is defined with ref or reactive
  • Computed props are defined with computed function
  • Methods are just normal functions
  • Script setup makes the syntax less verbose

Why Composition API?

Code Organization

Logic Reuse

Improved

TypeScript Support

Code Organization

Organize by OPTIONS

<script>
export default {
  data() {
    return {
      loading: false,
      products: [],
      sortBy: "price",
      desc: false,
    };
  },
  computed: {
    numberOfProducts() {...},
    sortedProducts() {...},
  },
  methods: {
    fetchProducts() {...},
    setSortDirection() {...},
  },
};
<script>

Product Fetching

Product Sorting

Product Meta

Features are all mixed up

Organize by LOGICAL CONCERN

<script setup>

// loading products
const loading = ref(true);
const products = ref([]);
async function fetchProducts() { ... }
fetchProducts();

// soring products
const sortBy = ref("price");
const desc = ref(false);
const sortedProducts = computed(...);

// meta
const numberOfProducts = computed(...);
</script>

Product Fetching

Product Sorting

Product Meta

Features grouped together

Which do you prefer?

<script setup>

// loading products
const loading = ref(true);
const products = ref([]);
async function fetchProducts() { ... }
fetchProducts();

// soring products
const sortBy = ref("price");
const desc = ref(false);
const sortedProducts = computed(...);

// meta
const numberOfProducts = computed(...);
</script>
<script>
export default {
  data() {
    return {
      loading: false,
      products: [],
      sortBy: "price",
      desc: false,
    };
  },
  computed: {
    numberOfProducts() {...},
    sortedProducts() {...},
  },
  methods: {
    fetchProducts() {...},
    setSortDirection() {...},
  },
};
<script>

❌

βœ…

Especially important for long/complex components

Questions?

πŸ™‹πŸΎβ€β™€οΈ

Exercise 4

πŸ‹οΈβ€β™€οΈ πŸ‹οΈ

Skip

Logic Reuse

Logic Reuse

<script setup>
import {ref} from "vue"
const count = ref(0);
const increment = () => count.value++;
</script>

<template>
<button @click="increment">{{ count }}</button>  
</template>

Button Component From Earlier

Logic Reuse

<script setup>
import {ref} from "vue"

// composable
const useCounter = ()=>{
  const count = ref(0);
  const increment = () => count.value++;  
  return { count, increment }
}

const {increment, count} = useCounter()

</script>

<template>
<button @click="increment">{{ count }}</button>  
</template>

Extract to a composition function (composable)

Logic Reuse

<script setup>
import {ref} from "vue"

// composable
const useCounter = (initial = 0)=>{
  const count = ref(initial);
  const increment = () => count.value++;  
  return { count, increment }
}

const {increment, count} = useCounter(40)

</script>

<template>
<button @click="increment">{{ count }}</button>  
</template>

Flexibility with arguments

Logic Reuse

// src/composables/useCounter.js
import {ref} from "vue"

export const useCounter = (initial = 0)=>{
  const count = ref(initial);
  const increment = () => count.value++;  
  return { count, increment }
}

Extract to it's own file that can be imported in any component

<script setup>
import {ref} from "vue"
import { useCounter } from "@/composables/useCounter"
  
  

  
  
  
  
  
 

const {increment, count} = useCounter(40)

</script>

<template>
<button @click="increment">{{ count }}</button>  
</template>

Logic Reuse

More practical example: useFetch composable

// @/composables/useFetch.js
import { ref } from 'vue';
export const useFetch = (url) => {
  const loading = ref(true);
  const data = ref(null);
  fetch(url).then(async (res) => {
    data.value = await res.json();
    loading.value = false;
  });
  return { data, loading };
};

Logic Reuse

More practical example: useFetch composable

<script setup>
import { useFetch } from '@/composables/useFetch';

const { data, loading } = useFetch(
  'https://someApiEndpoint.com'
);
</script>

<template>
  <p v-if="loading">loading...</p>
  <pre>{{ data }}</pre>
</template>

Logic Reuse

Use multiple times in same component without collisions

<script setup>
import { useFetch } from '@/composables/useFetch';

const { data: products, loading: loadingProducts } = useFetch(
  'https://someApiEndpoint.com'
);
  
const { data: users, loading: loadingUsers } = useFetch(
  'https://someOtherApiEndpoint.com'
);
</script>

Logic Reuse

Can also make the composable accept a reactive URL

// @/composables/useFetch.js
import { ref } from 'vue';
export const useFetch = (url) => {
  const loading = ref(true);
  const data = ref(null);
  fetch(url).then(async (res) => {
    data.value = await res.json();
    loading.value = false;
  });
  return { data, loading };
};

Logic Reuse

Can also make the composable accept a reactive URL

// @/composables/useFetch.js
import { ref, isRef, watch, computed } from 'vue';
export const useFetch = (url) => {

  // handle a reactive ref or a string
  const URL = computed(() => {
    return isRef(url) ? url.value : url;
  });
  const loading = ref(true);
  const data = ref(null);

  
  // wrap request in function to call right away
  // and whenever URL changes
  function makeRequest() {
    loading.value = true;
    fetch(URL.value).then(async (res) => {
      data.value = await res.json();
      loading.value = false;
    });
  }

  makeRequest();

  // watch URL for changes and make request 
  watch(URL, makeRequest);
  return { data, loading };
};

Logic Reuse

Can also make the composable accept a reactive URL

<script setup>
import {ref} from "vue"
import { useFetch } from '@/composables/useFetch';

const url = ref("https://someApiEndpoint.com")
const { data, loading } = useFetch(url);
  
</script>

Logic Reuse

Can also make the composable accept a reactive URL

<script setup>
import { ref, computed } from "vue"
import { useFetch } from '@/composables/useFetch';

const query = ref("") // whatever search string
const url = computed(()=>`https://someApiEndpoint.com?q=${query}`)
const { data, loading } = useFetch(url);
  
</script>

Logic Reuse

An example of a pointer position composable

// @/composables/usePointer.js
import { ref, onUnmounted } from 'vue';
export const usePointer = () => {
  const x = ref(0);
  const y = ref(0);
  
  // handler to call when the mouse move event fires
  const handler = (e) => {
    x.value = e.x;
    y.value = e.y;
  };
  
  // add handler to mousemove event
  window.addEventListener('mousemove', handler);
  
  // clean up when component is umnounted
  onUnmounted(() => {
    window.removeEventListener('mousemove', handler);
  });

  return { x, y };
};

Logic Reuse

An example of a pointer position composable

<script setup>
import { ref, computed } from 'vue';
import { usePointer } from './composables/usePointer';
const { x, y } = usePointer();
</script>

<template>
  <h1>x: {{ x }}, y: {{ y }}</h1>
</template>

Logic Reuse

Vue 2 did provide alternatives for logic re-use but they all had their drawbacks

  • Mixins
  • Higher Order Components
  • Renderless Components

Logic Reuse

Vue 2 did provide alternatives for logic re-use by they all had their drawbacks

Questions?

πŸ™‹πŸΎβ€β™€οΈ

Exercise 5&6

πŸ‹οΈβ€β™€οΈ πŸ‹οΈ

Logic Reuse

Off the Shelf Composables

VueUse

Collection of Vue Composition Utilities

VueUse

  • State
  • Elements
  • Browser
  • Sensors
  • Network
  • Animation
  • Component
  • Watch
  • Reactivity
  • Array
  • Time
  • Utilities

200+

VueUse

npm i @vueuse/core

VueUse

<script setup>
import {useMouse} from "@vueuse/core"
const { x, y } = useMouse()
</script>

<template>
  <div>pos: {{x}}, {{y}}</div>
</template>

useMouse

VueUse

<script setup>
import { useFetch } from '@vueuse/core'

const { isFetching, error, data } = await useFetch(url)
</script>

useFetch

VueUse

<script setup>
import { useLocalStorage } from '@vueuse/core';
const framework = useLocalStorage('framework', null);
</script>

<template>
  <input type="text" v-model="framework" />
</template>

useLocalStorage

VueUse

VueUse

Let's visit the website and see what else is available

Questions?

πŸ™‹πŸΎβ€β™€οΈ

Exercise 7

πŸ‹οΈβ€β™€οΈ πŸ‹οΈ

Recap

  • We've learned how to define reactive data, computed data, watchers, and methods in the composition API
  • Composition API is useful for better code organization
  • Also helpful for logic re-use
  • VueUse is a popular library of 200+ off-the-shelf composables

LifeCycle Hooks

LifeCycle Hooks

// MyComponent.vue
<script setup>
import { onMounted, onUnmounted } from "vue";
onMounted(()=>{
  // do things
})
</script>

Use in components

LifeCycle Hooks

// MyComponent.vue
<script setup>
import { onCreated } from "vue";
  
// just do what you'd normally do in created 
// directly in script setup

</script>

onCreated function does not exist

❌

LifeCycle Hooks

onCreated function does not exist

LifeCycle Hooks

// useMyComposable.js
import { onMounted, onUnmounted } from "vue";
export function useMyComposable(){
  
  onMounted(()=>{
    // do things
  })

}

Use in composables and will fire based on the lifecycle of the component used in

LifeCycle Hooks

// useMyComposable.js
import { onMounted, onUnmounted } from "vue";
export function useMyComposable(){
  
  onMounted(()=>{
    // DOM elements ready
  })

}

onMounted is useful for ensuring component elements are available in the DOM and can be directly accessed/manipulated

LifeCycle Hooks

// useMyComposable.js
import { onMounted, onUnmounted } from "vue";
export function useMyComposable(){
  
  onUnmounted(()=>{
    // clean up: event listeners,
    // intervals, etc
  })
}

onUnmounted is useful for cleaning up event listeners, intervals, etc to prevent memory leaks

Questions?

πŸ™‹πŸΎβ€β™€οΈ

Exercise 8

πŸ‹οΈβ€β™€οΈ πŸ‹οΈ

Skip

Ref vs Reactive

The Reactive Function

<script setup>
const framework = reactive({
  name: 'Vue',
  author:'Evan You',
  tags: ['javascript', 'vue']
})

framework.name // no need for .value
</script>

Alternative way to declare reactive data

Ref Works on Primitives

Reactive does not

// βœ… ref works
const framwork = ref('Vue')
const isAwesome = ref(true)
const created = ref(2014)

// ❌ won't work 
const framwork = reactive('Vue') 
const isAwesome = reactive(true)
const created = reactive(2014)

Can Re-assign whole ref values but not reactive

// βœ… ref works
let posts = ref(['post 1', 'post 2'])
posts.value = ['post 3', 'post 4']
// ❌ won't work 
let posts = reactive(['post 1', 'post 2'])
posts = ['post 3', 'post 4']

Ref requires .value reactive does not

// ref
const framework = ref({
  name: 'Vue',
  author:'Evan You',
  tags: ['javascript', 'vue']
})
                       ⬇️
console.log(framework.value.name)
// reactive
const framework = reactive({
  name: 'Vue',
  author:'Evan You',
  tags: ['javascript', 'vue']
})

console.log(framework.name)

Can destructure object of refs, cannot destructure a reactive object

// βœ… ref works 
const framework = {
  name: ref('Vue'),
  author: ref('Evan You'),
  tags: ref(['javascript', 'vue'])
}

const { name } = framework
// ❌ reactive doesn't
const framework = reactive({
  name: 'Vue',
  author:'Evan You',
  tags: ['javascript', 'vue']
})

// name is no longer reactive
const { name } = framework

Can convert reactive object to refs

// βœ… will work
const framework = toRefs(reactive({
  name: 'Vue',
  author:'Evan You',
  tags: ['javascript', 'vue']
}))

const { name } = framework

toRefs useful when exposing data defined with reactive from a composable

// @/composables/useFetch
import { reactive } from "vue"
export const useFetch = (url)=>{
  const state = reactive({
    loading: true,
    data: null
  })
  //...
  return toRefs(state)
  
}
<script setup>
import { useFetch } from "@/composables/useFetch"
const {data, loading} = useFetch("https://myApiEndpoint.com")
</script>

Which to use? πŸ€”

  • Sometimes you have no choice
    • Must use refs for primitives
    • Must use ref when replacing whole value
  • Otherwise it's personal preference
  • Many choose to use ref for everything

Ref or Reactive

Reactive State Helper Functions

Reactive State Helper Functions

let foo = ref("");
if (isRef(foo)) {
  // true
}

isRef()

Checks if a value is a ref object.

Reactive State Helper Functions

let foo = reactive({...})
if (isReactive(foo)) {
  // true
}

isReactive()

Checks if an object is a proxy created by reactive()

Reactive State Helper Functions

function useFoo(x: number | Ref<number>) {
  const unwrapped = unref(x)
  // unwrapped is guaranteed to be number now
}

unRef()

Returns the inner value if the argument is a ref, otherwise return the argument itself. This is a sugar function for val = isRef(val) ? val.value : val.

Reactive State Helper Functions

// returns existing refs as-is
toRef(existingRef)

// creates a readonly ref that calls the getter on .value access
toRef(() => props.foo)

// creates normal refs from non-function values
// equivalent to ref(1)
toRef(1)

toRef()

Can be used to normalize values / refs / getters into refs (3.3+).

const state = reactive({
  foo: 1,
  bar: 2
})

// a two-way ref that syncs with the original property
const fooRef = toRef(state, 'foo')

// mutating the ref updates the original
fooRef.value++
console.log(state.foo) // 2

// mutating the original also updates the ref
state.foo++
console.log(fooRef.value) // 3

toRef()

Can also be used to create a ref for a property on a source reactive object. The created ref is synced with its source property: mutating the source property will update the ref, and vice-versa.

Reactive State Helper Functions

toValue(1) //       --> 1
toValue(ref(1)) //  --> 1
toValue(() => 1) // --> 1

toValue()

Normalizes values / refs / getters to values. This is similar to unref(), except that it also normalizes getters. If the argument is a getter, it will be invoked and its return value will be returned.

Reactive State Helper Functions

const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)

// The ref and the original property is "linked"
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

toRefs()

Converts a reactive object to a plain object where each property of the resulting object is a ref pointing to the corresponding property of the original object.

Questions?

πŸ™‹πŸΎβ€β™€οΈ

Exercise 9

πŸ‹οΈβ€β™€οΈ πŸ‹οΈ

Skip

Exercise 10

πŸ‹οΈβ€β™€οΈ πŸ‹οΈ

Conclusion

πŸ’ͺ Compositon APIΒ  Benefits

  • can organize by logical concern
  • great for logic reuseΒ  Β 
  • better performance
  • no namespace collision and clear source of data (unlike mixins)
  • many new libs written for CAPI

🧩 Good Fit For

  • Re-using logic via composables
  • Very long components for organization
  • Everywhere if the whole team is on board
  • when TypeScript support is important

Recommended Video Courses

All courses included with your subscription!

Feel free to ask questions about topics in the courses and we usually respond in comments

Next Level Video Courses

All courses included with your subscription!

Feel free to ask questions about topics in the courses and we usually respond in comments

Other Workshops

Testing Fundamentals

and Vue Components

Learn the basics of unit testing and specifics for testing Vue.js components

State Management with Pinia

Learn how to use the officially recommended state management tool for Vue.js

Build Single Page Applications

Leverage the full power of the ecosystem to build a performant SPA (single page application).

TypeScript + Vue

Learn how to combine TypeScript with Vue.js for maintainable and scalable apps.

Other Great Resources

VueUse Source Code (great inspiration)

Other Great Resources

Vue Docs

(more composition functions mentioned there than in this workshop)

πŸ“© You will receive a Document with all the resources

πŸ“š

πŸ’ͺ

Help us improve

We'll be sending a survey

Q&A

πŸ™‹β€β™‚οΈπŸ™‹πŸΎβ€β™€οΈ

Thank You!

πŸ™

Vue 3 Composition API (v3)

By Daniel Kelly

Vue 3 Composition API (v3)

  • 861