@vueschool_io
vueschool.io
Vue 2 ย
New Features and Migrating
3
๐ค
Daniel Kelly
Lead Instructor @ Vue School
Full Stack developer (10 years)
Husband and Father
Workshop Overview
- Intro to Vue 3
- Benefits of Vue 3
- Improvements to Existing Features
- Removed Features
- New Features
- Composition API (overview)*
- Teleport
- State Driven CSS*
- Other CSS Enhancements
- Emits Declaration
- Suspense
- Changes in the Ecosystem
- Migration
* backported to Vue 2.7
Intro to Vue 3
๐
Pinia Experiment Begins
Nov, 2019
RFC Repo Created
Jan 14, 2019
Vue 3 Codebase Goes Public
Jan 3, 2020
Vue 3 Soft Launch
(One Piece)
Sep 18, 2020
Vue 3 New Default
Jan 20, 2022
Evan Announced Dev on Vite.js
Jan 12, 2020
๐ฃ
Vue 3 Development Announced
Feb 2018
v3
Volar
<script setup>
more!
Why Vue 3?
Benefits and why upgrade
Smaller and Faster
- Initial render up to 55% faster
- Memory usage down 54%
- Updates up to 133% faster
- Unused features are tree-shakeable (like transitions, nextTick, etc)
Faster === better conversions
A one-second site speed improvement can increase mobile conversions by up to 27%.
===
Write More Maintainable Code
- Better logic re-use and organization with the Composition API *
- Type-safety with improved TypeScript Support
(TypeScript is fast becoming the norm for large scale JavaScript apps) - Other new features and API adjustments that make code more intuitive
* backported to Vue 2.7
More Maintainable Code is Good For Everyone!
โ
Stakeholders
ย ย ย ย Faster turn around times, less back and forth
โ
Site Visitors/Customers
ย ย ย ย Less bugs, more features faster
โ Developers
ย ย ย ย Better DX, Iterate faster, spend less time debugging
Supported by the Latest and Greatest Libraries
- Some of the newest 3rd party plugins, frameworks, etc only support Vue 3
- ie. FormKit, Nuxt 3, Vuetify 3, etc
Much of the Same Syntax We Already Love โค๏ธ
- Options API Still 100% Supported
- Single File Components
(script, template, style) - Declarative Template Syntax
Vue 2 EOL is Dec 2023
Improvements to Existing Features
Improvements to Existing Features
- Multiple v-models (and more!)
- No more reactivity caveats ๐
- Fragments
- Comprehensive $attrs
Multiple v-models
// MyInput.vue
<template>
<input
@input="$emit('input', $event.target.value)"
:value="value"
/>
</template>
<script>
export default {
props:{
value: String
}
}
</script>
Support v-model on a component by emitting input and accepting a value prop
v2
<MyInput v-model="name" />
Multiple v-models
// MyInput.vue
<template>
<input
@input="$emit('update:modelValue', $event.target.value)"
:value="modelValue"
/>
</template>
<script>
export default {
props:{
modelValue: String
}
}
</script>
Support v-model on a component by emitting update:modelValue and accepting a modelValue prop
v3
<MyInput v-model="name" />
Multiple v-models
// MyInput.vue
<template>
<input
@input="$emit('update:modelValue', $event.target.value)"
:value="modelValue"
/>
<input
@input="$emit('update:email', $event.target.value)"
:value="title"
/>
<input
@input="$emit('update:password', $event.target.value)"
:value="title"
/>
</template>
<script>
export default {
props:{
modelValue: String,
email: String,
password: String,
}
}
</script>
Provide argument to v-model to specify any prop/update:[prop]
v3
<MyInput
v-model="name"
v-model:email="email"
v-model:password="password"
/>
Multiple v-models
<MyComponent
:title="pageTitle"
@update:title="pageTitle = $event"
/>
<!-- Shortand for Above -->
<MyComponent :title.sync="pageTitle" />
Multiple v-models replace .sync
v2
Multiple v-models
<MyComponent
:title="pageTitle"
@update:title="pageTitle = $event"
/>
<!-- Shortand for Above -->
<MyComponent v-model:title="pageTitle" />
Multiple v-models replace .sync
v3
v-model anatomy
Custom v-model modifiers
<MyComponent v-model.capitalize="myText" />
Vue 3 also allows us to create custom modifiers for v-model
Custom v-model modifiers
<!-- MyComponent.vue-->
<script>
export default {
props: {
//...
modelModifiers: {
default: () => ({})
}
},
created() {
console.log(this.modelModifiers) // { capitalize: true }
}
//...
}
</script>
Check for them on the modelModifiers prop. Added modifiers will be a boolean true
<MyComponent v-model.capitalize="myText" />
Custom v-model modifiers
<!-- MyComponent.vue-->
<script>
export default {
//...
methods: {
emitValue(e) {
let value = e.target.value
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
this.$emit('update:modelValue', value)
}
}
}
</script>
Then you can alter the emitted value based on the modifier
No More Reactivity Caveats
Vue.set(this.frameworks, index, 'Vue')
v2
this.frameworks[index] = "Vue"
v3
Setting a new array item
No More Reactivity Caveats
Vue.set(this.framework, 'name', 'Vue')
v2
this.framework.name = "Vue"
v3
Adding a new object property
No More Reactivity Caveats
Vue.delete(this.framework, 'caveats')
v2
delete this.framework.caveats
v3
Deleting an object property
No More Reactivity Caveats
Less Caveats === More Intuitive === Less Bugs
Fragments
aka. Multiple Root Elements
v2
<template>
<div>...</div>
<div>...</div>
</template>
โ
If you did this...
Fragments
aka. Multiple Root Elements
v2
<template>
<div>...</div>
<div>...</div>
</template>
You'd get an error like this!
Fragments
aka. Multiple Root Elements
v2
<template>
<div>
<div>...</div>
<div>...</div>
</div>
</template>
And have to wrap everything with a div
(which sometimes causes styling issues)
Fragments
aka. Multiple Root Elements
v3
<template>
<div>...</div>
<div>...</div>
</template>
It's no problem! ๐
Fragments
aka. Multiple Root Elements
v3
<template>
<div>...</div>
<div v-bind="$attrs">...</div>
</template>
And you can specify where to put the fall-through attributes
Comprehensive $attrs
v2
<template>
<label>
<input type="text" v-bind="$attrs" v-on="$listeners" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
Listeners separate from $attrs and if you wanted them to fall through you must remember to bind seperately
Comprehensive $attrs
v3
//MyInput.vue
<template>
<label>
<input type="text" v-bind="$attrs" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
Listeners combined with $attrs. Defined as functions prefixed with on
<MyInput id='my-input' @close="..."/>
$attrs === {
id: 'my-input',
onClose(){
...
}
}
Comprehensive $attrs
v2
// MyInput.vue
<template>
<label>
<input type="text" v-bind="$attrs" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
Class and Style separate from $attrs
Given this component definition...
Comprehensive $attrs
v2
<MyInput id="my-id" class="my-class" />
Class and Style separate from $attrs
And used like this...
<label class="my-class">
<input type="text" id="my-id" />
</label>
Would render this HTML
Comprehensive $attrs
v3
<MyInput id="my-id" class="my-class" />
Class and Style included with $attrs
And used like this...
<label>
<input type="text" id="my-id" class="my-class" />
</label>
Would render this HTML
Questions?
๐๐พโโ๏ธ
Assignment # 1
๐ฉโ๐ป๐จ๐ฝโ๐ป
โฐ 20 mins
โ Removed Features
โ Removed Features
- Functional Components
- Filters
- Keycode Modifiers
- $children
Functional Components
<template functional>
<component
:is="`h${props.level}`"
v-bind="attrs"
v-on="listeners"
>
<slot></slot>
</component>
</template>
<script>
export default {
props: ["level"],
};
</script>
v2
In v2, functional components provided a more performant alternative when component state was not needed
No $ prefix
Functional Components
<template>
<component
:is="`h${props.level}`"
v-bind="$attrs"
>
<slot></slot>
</component>
</template>
<script>
export default {
props: ["level"],
};
</script>
v3
In v3, the performance difference for stateful components is negligible so functional SFC's are removed
$ prefix
No functional
keyword
Functional Components
<script>
// note that h is imported from vue
// instead of provided as an argument to the render function
// (another diff between v2 and v3)
import { h } from "vue";
// functional components is a render function
const DynamicHeading = (props, context) => {
return h(`h${props.level}`, context.attrs, context.slots);
};
DynamicHeading.props = ["level"];
export default DynamicHeading;
</script>
v3
Or you can define functional components as plain functions
ย
Filters
<template>
<h1>Bank Account Balance</h1>
<p>{{ accountBalance | currencyUSD }}</p>
</template>
<script>
export default {
//...
filters: {
currencyUSD(value) {
return '$' + value
}
}
}
</script>
v2
In v2, you could use filters to format data in the template
ย
Custom syntax that involves:
- a learning curve
- and implementation costs
Filters
<template>
<h1>Bank Account Balance</h1>
<p>{{ accountInUSD }}</p>
</template>
<script>
export default {
//...
computed: {
accountInUSD() {
return '$' + this.accountBalance
}
}
}
</script>
v3
In v3, filters are removed. Just use a computed prop instead.
ย
Global Filters
const app = createApp(App)
app.config.globalProperties.$filters = {
currencyUSD(value) {
return '$' + value
}
}
v3
In v3, global filters can be replaced with globally defined methods
ย
<template>
<h1>Bank Account Balance</h1>
<p>{{ $filters.currencyUSD(accountBalance) }}</p>
</template>
KeyCode Modifiers
v2
In Vue 2 you could use keycodes as modifiers for keyboard events
ย
<!-- keycode 75 === 'k' -->
<input v-on:keyup.75="doThing" />
KeyCode Modifiers
v3
In Vue 3 you use the event's key value instead
<input v-on:keyup.k="doThing" />
<input v-on:keyup.k="doThing" />
KeyCode Modifiers
v3
For multi-word key names you'll use the kebab case
<input v-on:keyup.arrow-down="doThing" />
KeyCode Modifiers
v3
The keys for some punctuation marks can just be included literally.
(This excludes "
, '
, /
, =
, >
, and .
You can check these on the event in your handler)
<input v-on:keyup.,="commaPress" />
KeyCode Modifiers
KeyboardEvent.keyCode is now deprecated in browsers and no longer recommended for use. Therefore Vue 3 removes them.
ย
<input v-on:keyup.k="doThing" />
$children
v2
<template>
<div>
<my-button>Change logo</my-button>
</div>
</template>
<script>
import MyButton from './MyButton'
export default {
components: { MyButton },
mounted() {
console.log(this.$children[0]) // VueComponent (MyButton)
}
}
</script>
In Vue 2, you could access all a components child components via $children
ย
$children
v3
In Vue 3, $children no longer exists. If you need access to a component via the parent you can use a template ref instead
ย
<template>
<div>
<my-button ref="button">Change logo</my-button>
</div>
</template>
<script>
import MyButton from './MyButton'
export default {
components: { MyButton },
mounted() {
console.log(this.$refs.button) // VueComponent (MyButton)
}
}
</script>
Questions?
๐๐พโโ๏ธ
Assignment #2&3
๐ฉโ๐ป๐จ๐ฝโ๐ป
โฐ 15 mins
Skip
10 Minute Break
โ๏ธ๐
โฐ 10 mins
๐ New Features
๐ New Features
- Composition API *
- Teleport
- State Driven CSS *
- Other CSS Enhancements
- Emits Declaration
- Suspense
* backported to Vue 2.7
Composition API
Code Organization
Logic Reuse
Improved
TypeScript Support
Composition API
Full Workshop Dedicated to CAPI
Composition API
โก๏ธ Super Quick Overview
new Vue({
data(){
return {
loading: false,
count: 0,
user: {}
}
},
computed: {
double () { return this.count * 2 },
fullname () {/* ... */}
},
methods: {
increment () { this.count++ },
fetchUser () {/* ... */}
}
})
import { ref, computed, watch } from 'vue'
No longer passing options INTO Vue
Instead we're getting reactive functions OUT OF Vue
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 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
Composition API
Full Workshop Dedicated to CAPI
Teleport
- Allows us to keep markup inside a component but visually display it elsewhere in the DOM
- Great for modal overlays
Teleport
Modal overlay without teleport usually relies on fixed positioning
<template>
<button @click="open = true">Open Modal</button>
<div v-if="open" class="modal">
<slot></slot>
<footer>
<button @click="open = false">Close</button>
</footer>
</div>
</template>
<script>
// ....
</script>
<style scoped>
.modal {
@apply fixed;
}
</style>
- Position fixed only works if an ancestor doesn't include `transform`, `perspective`, or `filter`
- z-index issues
v2
Teleport
Append markup to any arbitrary element no matter where the component lives
<template>
<button @click="open = true">Open Modal</button>
<Teleport to="body">
<div v-if="open" class="modal">
<slot></slot>
<footer>
<button @click="open = false">Close</button>
</footer>
</div>
</Teleport>
</template>
<script>
// ....
</script>
<style scoped>
.modal {
@apply absolute;
}
</style>
v3
Teleport Tips
`to` prop takes a CSS selector or an actual DOM node
<template>
<!-- string query selector-->
<Teleport to="body">
<!--or actual element-->
<Teleport :to="body">
</template>
<script>
export default {
data() {
return {
body: document.querySelector("body"),
};
},
};
</script>
Teleport Tips
can combine <Teleport> with <Transition>
<template>
<Teleport :to="body">
<Transition>
<div v-if="open" class="modal">
//...
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
Teleport Tips
Can have multiple teleports active at one time.
<Teleport to="#modals">
<div>A</div>
</Teleport>
<Teleport to="#modals">
<div>B</div>
</Teleport>
<div id="modals">
<div>A</div>
<div>B</div>
</div>
Teleport Tips
Can dynamically disable. Useful to conditionally display inside component
<Teleport :disabled="isMobile">
...
</Teleport>
Teleport Tips
ย The teleport `to` target must already exist in the DOM when the component using <Teleport> is mounted.
- Ideally, this is an element outside the Vue app.
- When targeting elements rendered by Vue, ensure it's mounted first
Questions?
๐๐พโโ๏ธ
Assignment #4
๐ฉโ๐ป๐จ๐ฝโ๐ป
โฐ 25 mins
Lunch
๐
โฐ 25 mins
Lunch Break
๐ฝ๏ธ
โฐ 25 mins
State Driven CSS
and more!
State Driven CSS
In v2, inline styles were the only option for dynamic CSS rules
<template>
<div
class="swatch"
:style="{ backgroundColor: color }"
></div>
</template>
<script>
export default{
data(){
return {
color: "red"
}
}
}
</script>
v2
State Driven CSS
Re-use would mean re-declaring the inline style
<template>
<div
class="swatch"
:style="{ backgroundColor: color }"
></div>
<div
class="swatch"
:style="{ backgroundColor: color }"
></div>
</template>
<script>
export default{
data(){
return {
color: "red"
}
}
}
</script>
v2
State Driven CSS
In v3, you can bind CSS values directly in the style block
v3
<template>
<div>
<div class="swatch"></div>
<div class="swatch"></div>
</div>
</template>
<script>
export default{
data(){
return { color: "red" }
}
}
</script>
<style scoped>
.swatch{
background-color: v-bind(color);
}
</style>
State Driven CSS
Defines a hashed CSS custom property on the root component element
(or on each element if no root)
v3
<template>
<div>
<div class="swatch"></div>
<div class="swatch"></div>
</div>
</template>
<script>
export default{
data(){
return { color: "red" }
}
}
</script>
<style scoped>
.swatch{
background-color: v-bind(color);
}
</style>
State Driven CSS
Can use dot notation to target nested data (must use quotes)
v3
<template>
<div class="swatch"></div>
<div class="swatch"></div>
</template>
<script>
export default{
data(){
return {
colors:{ primary: "red" }
}
}
}
</script>
<style scoped>
.swatch{
background-color: v-bind('color.primary');
}
</style>
State Driven CSS
v-bind must the entire value
v3
<template>
<div class="swatch"></div>
<div class="swatch"></div>
</template>
<script>
export default{
data(){
return {
colors:{ primary: "red" },
width: 100
}
},
computed:{
widthInPixels(){ return this.width + 'px'}
}
}
</script>
<style scoped>
.swatch{
background-color: v-bind('color.primary');
width: v-bind(width)px; /* โ This won't work */
width: v-bind(widthInPixels);
}
</style>
Vue Specific CSS Selectors
v3
<style scoped>
/* Style child components */
.a :deep(.b) {}
/* Style slot content */
:slotted(div) {}
/* Quickly define a global rule */
:global(.red) {}
</style>
<style>
/* Same as :global in scoped tag above */
.red{}
</style>
Questions?
๐๐พโโ๏ธ
Assignment #5
๐ฉโ๐ป๐จ๐ฝโ๐ป
โฐ 15 mins
Emits Option
Declare Component Events
Emits Option
Emits scattered throughout component
<!-- DataSender.vue -->
<template>
<div>
<button @click="sendData">Send Data</button>
</div>
</template>
<script>
export default {
methods: {
sendData() {
this.$emit('sending-start')
// send data to API
this.$emit('sending-complete')
}
}
}
</script>
v2
Emits Option
Declare all events with emits option
<!-- DataSender.vue -->
<template>
<div>
<button @click="sendData">Send Data</button>
</div>
</template>
<script>
export default {
emits:['sending-start', 'sending-complete'],
methods: {
sendData() {
this.$emit('sending-start')
// send data to API
this.$emit('sending-complete')
}
}
}
</script>
v3
Emits Option
Benefits of Emit Option
- It documents component events in a single place
<script>
export default {
emits:['sending-start', 'sending-complete'],
//...
}
</script>
Important for devs and tooling
Emits Option
Benefits of Emit Option
- When using TypeScript events appear in intellisense results
Emits Option
Benefits of Emit Option
- Makes the removal of $listeners possible
- Why? because listeners for declared events are removed from the `$attrs`
- This also makes means no more .native
<MyButton @click.native="doThing" />
Emits Option
Benefits of Emit Option
- Documents component events in a single place
- When using TypeScript events appear in intellisense results
ย - Listeners for declared events are removed from the `$attrs`
- Can validate emit payload
Emits Option
Validate event payload
<!-- DataSender.vue -->
<!--...-->
<script>
export default {
// define as object to use validation
emits:{
// null is no validation
'sending-start': null,
// or provide function to validate
// return truthy for valid, falsy for invalid
'sending-complete'(payload){
return !!(typeof payload.duration === 'number' && payload.response);
}
},
methods: {
sendData() {
this.$emit('sending-start')
// send data to API
this.$emit('sending-complete', {
duration: 2,
// response: 'how are you'
})
}
}
}
</script>
v3
Emits Option
Validate event payload with TypeScript
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
emits: {
addBook(payload: { bookName: string }) {
// perform runtime validation
return payload.bookName.length > 0
}
},
methods: {
onSubmit() {
this.$emit('addBook', {
bookName: 123 // Type error!
})
this.$emit('non-declared-event') // Type error!
}
}
})
</script>
v3
Questions?
๐๐พโโ๏ธ
Assignment #6
๐ฉโ๐ป๐จ๐ฝโ๐ป
โฐ 20 mins
Skip
Suspense
What is Suspense?
Admittedly a little difficult to understand if you've never heard of it before
Suspense
Orchestrate async dependencies
Without Suspense
With Suspense
With Suspense
ย
(with more control at component level)
AKA - eliminate popcorn loading
Suspense
In Vue 2 you'd have local loading data in
the component
<!-- PostsList.vue -->
<script>
export default{
data(){
return {
loading: true,
posts: null
}
},
async created(){
const res = await fetch("https://myapi.com/posts")
this.posts = await res.json()
this.loading = false
}
}
</script>
<template>
<div>
<AppSpinner v-if="loading"/>
<div v-else>...</div>
</div>
</template>
v2
Suspense
Somewhere higher in the higharchy would use the component and others like it
<!-- ParentComponent.vue -->
<template>
<div>
<PostsList />
<UsersList />
<CommentsList />
</div>
</template>
v2
Suspense
Let's take the same component and
modify it for Vue 3
<!-- PostsList.vue -->
<script>
export default{
data(){
return {
loading: true,
posts: null
}
},
async created(){
const res = await fetch("https://myapi.com/posts")
this.posts = await res.json()
this.loading = false
}
}
</script>
<template>
<div>
<AppSpinner v-if="loading"/>
<div v-else>...</div>
</div>
</template>
v2
Suspense
In Vue 3 you can get rid of all that loading state in the component
<!-- PostsList.vue -->
<script>
export default{
data(){
return {
posts: null
}
},
async created(){
const res = await fetch("https://myapi.com/posts")
this.posts = await res.json()
}
}
</script>
<template>
<div>
<div>...</div>
</div>
</template>
v3
Suspense
Then return an async setup function
instead
<!-- PostsList.vue -->
<script>
export default{
async setup(){
const res = await fetch("https://myapi.com/posts")
const posts = await res.json()
return { posts }
},
}
</script>
<template>
<div>
<div>...</div>
</div>
</template>
v3
Don't worry too much about how setup works right now
Suspense
With script setup you can use a top level await
<!-- PostsList.vue -->
<script setup>
const res = await fetch("https://myapi.com/posts")
const posts = await res.json()
</script>
<template>
<div>
<div>...</div>
</div>
</template>
v3
Don't worry too much about how setup works right now
Suspense
Then in the parent you use suspense to show a
single loader once all promises have resolved
<!-- ParentComponent.vue -->
<template>
<div>
<Suspense>
<!-- Put all the async components
inside the default slot -->
<template #default>
<PostsList />
<UsersList />
<CommentsList />
</template>
<!-- And your loader
in the fallback slot -->
<template #fallback>
<AppSpinner/>
</template>
</Suspense>
</div>
</template>
v3
Suspense
Also works with asyncComponents
(different chunk from main js bundle)
<script>
import { defineAsyncComponent } from 'vue'
export default {
components: {
AdminPage: defineAsyncComponent(() =>
import('./components/AdminPageComponent.vue')
)
}
}
</script>
<template>
<Suspense>
<template #default> <AdminPage /> </template>
<template #fallback> <AppSpinner/> </template>
</Suspense>
</template>
v3
Suspense
Total control by mixing async setup and local loading state
<!-- ParentComponent.vue -->
<template>
<div>
<Suspense>
<template #default>
<!-- has async setup -->
<PostsList />
<!-- has async setup -->
<UsersList />
<!-- handles own loading state -->
<CommentsList />
</template>
<template #fallback>
<AppSpinner/>
</template>
</Suspense>
</div>
</template>
v3
Suspense
Does it do error handling?
Suspense Recap
- Eliminates "Popcorn loading"
- Less boilerplate in components (no loading state)
- Handle loading state at top level
- Can still handle loading state in component if desired for total control
Suspense Word of Warning
Suspense is technically an experimental feature
Suspense Word of Warning
But Nuxt 3 uses in production... so I wouldn't
worry about Vue.js core making breaking changes
Questions?
๐๐พโโ๏ธ
Assignment #7
๐ฉโ๐ป๐จ๐ฝโ๐ป
โฐ 20 mins
5 Minute Break
โ๏ธ๐
โฐ 5 mins
Migrate to Vue 3
Migrate to Vue 3
- All the great new features we've already talked about!
- Some improvements mean breaking changes that we haven't seen yet
How do I Migrate to V3?
How do I Migrate to V3?
The Official Migration guide provides:
- A list of breaking changes complete with examples of prior behavior, new behavior, and a strategy for migrating
Breaking Changes
Example of Filter
Notable Breaking Changes
Besides the breaking changes already seen throughout the workshop, there are a few more worthy of noting
๐
Notable Breaking Changes
Async compnent syntax changed
<!-- Vue 2 -->
<script>
export default{
components:{
MyAsyncComponent: () => import('./MyAsyncComponent.vue')
}
}
</script>
<!-- Vue 3 -->
<script>
import {defineAsyncComponent} from "vue"
export default{
components:{
MyAsyncComponent: defineAsyncComponent(
() => import('./MyAsyncComponent.vue')
)
}
}
</script>
Watch on Arrays
<script>
export default{
data(){
return {
food: ['Hamburger', 'Hotdog', 'Spaghetti', 'Taco']
}
},
watch:{
// Vue 2 - will fire when array replaced, order changed, item added, etc
// Vue 3 - will only fire when array is replaced
food(){
console.log('food changed')
},
// Vue 3 - must provide for handler to fire on order changed, item added, etc
food:{
handler(){
console.log('food changed')
},
deep: true
}
}
}
</script>
Notable Breaking Changes
Global API Treeshaking
<!-- Vue 2 -->
<script>
import Vue from 'vue'
Vue.nextTick(() => {
// something DOM-related
})
</script>
<!-- Vue 3 -->
<script>
import { nextTick } from 'vue'
nextTick(() => {
// something DOM-related
})
</script>
Notable Breaking Changes
Global API Treeshaking
<!-- Vue 2 -->
<script>
import Vue from 'vue'
Vue.nextTick(() => {
// something DOM-related
})
</script>
<!-- Vue 3 -->
<script>
import { nextTick } from 'vue'
nextTick(() => {
// something DOM-related
})
</script>
Notable Breaking Changes
Also Vue.version
<!-- Vue 2 -->
<script>
import Vue from 'vue'
console.log(Vue.version)
</script>
<!-- Vue 3 -->
<script>
import { version } from 'vue'
console.log(version)
</script>
Custom Directives Hooks
// Vue 2
Vue.directive('highlight', {
bind(el, binding, vnode) {
el.style.background = binding.value
}
})
// Vue 3
const app = Vue.createApp({})
app.directive('highlight', {
beforeMount(el, binding, vnode) {
el.style.background = binding.value
}
})
Notable Breaking Changes
custom directive hooks mirrors component lifecycle hooks
Custom Directives Hooks
Notable Breaking Changes
- created - new! This is called before the element's attributes or event listeners are applied.
- bind โ beforeMount
- inserted โ mounted
- beforeUpdate: new! This is called before the element itself is updated, much like the component lifecycle hooks.
- update โ removed! There were too many similarities to
updated
, so this is redundant. Please useupdated
instead. - componentUpdated โ updated
- beforeUnmount: new! Similar to component lifecycle hooks, this will be called right before an element is unmounted.
- unbind -> unmounted
Notable Breaking Changes
All breaking changes can be found at:
How do I Migrate to V3?
ย
- A list of breaking changes complete with examples of prior behavior, new behavior, and a strategy for migrating
- A list of New Framework-level Recommendations
The Official Migration guide provides:
How do I Migrate to V3?
New Framework Level Recomendations
- Vue Core v2 โก v3
โ Yes, this is the priority
๐ฅ
๐จ
๐จ
๐ฅ
-
Options โก Composition
โก๏ธ Recommend using CAPI going forward as needed
- Vue CLI โก Vite
๐ Sooner or later
- Vuex โก Pinia
๐ Sooner or later
Difficulty
๐ฉ Easy
๐จ Medium
๐ฅ Hard
- Router, Devtools, test-utils
๐ฉ
๐ฉ
- Vetur โก Volar
โ Yes for VS Code, super easy
Resource for Other Migrations
- Options โก Composition
- Fairly 1-to-1 Change
- Vue.js 3 Master Class
- Vue CLI โก Vite
- Vuex โก Pinia
How do I Migrate to V3?
ย
- A list of breaking changes complete with examples of prior behavior, new behavior, and a strategy for migrating
- A list of New Framework-level Recommendations
- Migration build
The Official Migration guide provides:
What is the Migration Build?
- @vue/compat
- a build of Vue 3 that supports Vue 2 API's (mostly)
- Usage of features that have changed or been deprecated in Vue 3 will emit runtime warnings.
- Used for migrating from Vue 2 to Vue 3
Install Vue 3 and Migration Build
npm install vue@3.2.45
npm install @vue/compat
Step
1
Alias vue to @vue/compat
Step
2
// vue.config.js (vue-cli)
module.exports = {
chainWebpack: config => {
config.resolve.alias.set('vue', '@vue/compat')
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
return {
...options,
compilerOptions: {
compatConfig: {
MODE: 2
}
}
}
})
}
}
Vue Docs also contains example configs for:
- Plain Webpack
- Vite
Vue CLI
Handle compile time errors
Step
3
- Errors will display in the terminal when running `npm run serve`
- Is recommended to handle all compile time errors first
Handle compile time errors
Step
3
- Errors will display in the terminal when running `npm run serve`
- Is recommended to handle all compile time errors first
Set compiler to Vue 3 mode
Step
4
module.exports = {
//...
compilerOptions: {
compatConfig: {
MODE: 2,
MODE: 3
},
},
}
Means the Vue compiler should expect all compiler level code to be written as per Vue 3 specification
Set compiler to Vue 3 mode
Step
4
module.exports = {
//...
compilerOptions: {
compatConfig: {
MODE: 2,
MODE: 3
},
},
}
Means the Vue compiler should expect all compiler level code to be written as per Vue 3 specification
More on compatConfig in a minute!
Update dependencies like Vue Router and Vuex
Step
5
Official dependencies will have their own migration guides. Others may or may not. (This is where things can get a little dicey, but the ecosystem has definitely progressed)
Run app and go through console warnings
Step
6
- Deprecations will show in console
- Can filter warnings in console to concentrate on one type of change at a time
IDs for warnings found here:
https://v3-migration.vuejs.org/migration-build.html#feature-reference
ID
Explanation
ย Link to docs
Run app and go through console warnings
Step
6
- Once you've handled all instances of a feature ID you can turn compatibility for it off
- All your tests involving that feature should now pass and be 100% compatible with Vue 3
// main.js
import { configureCompat } from 'vue'
// disable compat for certain features
configureCompat({
COMPONENT_V_MODEL: false,
FEATURE_ID_B: false
})
Run app and go through console warnings
Step
6
- you could also do the same config at the component level
- Use case for this? ๐คท Maybe you could migrate one component at a time as opposed to one feature ID at a time
export default {
compatConfig: {
COMPONENT_V_MODEL: false,
FEATURE_ID_B: false
}
// ...
}
Run app and go through console warnings
Step
6
- Can deploy to production with partial migration (warnings still in console)
- Minimal performance hit
- HOWEVER, I recommend a git branch for complete migration and deploy all together
feature/vue-3-migration
and maybe a commit per feature ID?
Run app and go through console warnings
Step
6
- when all console warnings are gone you'll end up with a compatConfig that's disabled all applicable feature ID's
- but you'll have one feature ID left to handle: TRANSITION_CLASSES
configureCompat({
COMPONENT_V_MODEL: false,
GLOBAL_MOUNT_CONTAINER: false,
GLOBAL_MOUNT: false,
GLOBAL_SET: false,
//...
})
Update transition class names (no console warning)
Step
7
- You can do a project-wide search in IDE for .*-enter and .*-leave CSS class names
ย - Replace ย ย ย ย ย ย ย ย ย ย ย with
ย - Replace ย ย ย ย ย ย ย ย ย ย ย with
.v-enter
.v-enter-from
.v-leave
.v-leave-from
Update transition class names (no console warning)
Step
7
- add TRANSITION_CLASSES to compatConfig
configureCompat({
// ...
TRANSITION_CLASSES: false
})
Update transition class names (no console warning)
Step
7
- once all the IDs have had compatibility turned off that's the equivalent of changing the compatConfig.mode to 3
configureCompat({
MODE: 3
})
Switch to Vue 3 proper
Step
8
- uninstall @vue/compat
- remove any compat config
- remove alias of vue to @vue/compat
// package.json
"@vue/compat": "^3.1.0",
// vue.config.js
module.exports = {
chainWebpack: (config) => {
config.resolve.alias.set("vue", "@vue/compat");
config.module
.rule("vue")
.use("vue-loader")
.tap((options) => {
return {
...options,
compilerOptions: {
compatConfig: {
MODE: 3,
},
},
};
});
},
};
import {
createApp,
configureCompat
} from "vue";
import App from "./App.vue";
configureCompat({
COMPONENT_V_MODEL: false,
GLOBAL_MOUNT_CONTAINER: false,
GLOBAL_MOUNT: false,
GLOBAL_SET: false,
//...
})
1
2
3
Questions?
๐๐พโโ๏ธ
HOMEWORK!
๐ฉโ๐ป๐จ๐ฝโ๐ป
Assignment #8
๐ฉโ๐ป๐จ๐ฝโ๐ป
โฐ 30 mins
Do you accept this challenge? ๐ช
Questions?
๐๐พโโ๏ธ
General Q&A
โ
โฐ 15 minutes
Thank You!
๐
See you tomorrow!
Vue 3 New Features
By Daniel Kelly
Vue 3 New Features
- 873