Welcome to our deep dive into the dynamic world of Vue 3, the latest iteration of the progressive JavaScript framework that has taken the development community by storm. Vue 3 brings a plethora of exciting features and improvements that not only enhance the performance but also make the development process more efficient and enjoyable.
In this comprehensive guide, we'll traverse the breadth of Vue 3, from its novel Composition API to the core concepts of reactivity, components, directives, and routing. We will demystify these topics with plenty of code examples and useful tips, ensuring you have a solid understanding by the end of your reading journey.
As we venture further, we'll also touch upon state management in Vue 3 and explore Pinia, a nimble and intuitive alternative to Vuex. We'll show you how to leverage Pinia's power to make your applications more robust and maintainable.
Whether you're a newcomer to Vue or a seasoned developer looking to upgrade your skills, this guide is designed to equip you with the knowledge and confidence to build amazing Vue 3 applications. So, buckle up and let's dive right in!
1. Basic Vue 3 App:
Vue 3 applications are initialized slightly differently from Vue 2. Here's a basic Vue 3 application:
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
This is how you create a new Vue 3 application and mount it to an HTML element with id app
.
2. Vue Component:
Here's an example of a simple Vue 3 component:
import { defineComponent } from 'vue';
export default defineComponent({
name: 'MyComponent',
data() {
return {
message: 'Hello Vue 3!'
}
}
})
This code defines a new component called MyComponent
. The data
function returns an object with the component's data.
3. Vue Props:
Props are custom attributes you can register on a Vue component:
import { defineComponent } from 'vue';
export default defineComponent({
name: 'ChildComponent',
props: {
message: String,
},
})
In this code, we're defining a prop message
which expects a String
value.
4. Vue Directives:
Vue.js uses double braces {{ }}
as place-holders for data. Vue.js directives are HTML attributes with the prefix v-
<template>
<div>
<!-- This will print the message data property -->
<p>{{ message }}</p>
<!-- This will bind the title attribute of the p element to the message data property -->
<p v-bind:title="message"></p>
<!-- This will conditionally render the p element, only if showMessage is true -->
<p v-if="showMessage">{{ message }}</p>
<!-- This will handle the click event, calling the updateMessage method -->
<button v-on:click="updateMessage">Update</button>
</div>
</template>
5. Vue Options API vs Composition API:
In Vue 3, we have the new Composition API as an alternative to the Options API for organizing logic code in a component. Here's a comparison:
Options API:
import { defineComponent } from 'vue';
export default defineComponent({
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++;
}
},
});
Composition API:
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const count = ref(0);
function increment() {
count.value++;
}
return {
count,
increment,
}
}
});
Both codes do the same thing - increment a counter, but the Composition API provides a more flexible way to compose component logic.
6. Vue 3 Lifecycle Hooks:
Lifecycle hooks in Vue 3 have slightly different names from Vue 2. They are prefixed with "on" instead of using the "before" prefix and camelCase convention. For instance, beforeCreate
becomes onBeforeMount
in Vue 3.
import { onMounted, onUpdated, onUnmounted } from 'vue';
export default {
setup() {
onMounted(() => {
console.log('Component is mounted');
});
onUpdated(() => {
console.log('Component is updated');
});
onUnmounted(() => {
console.log('Component is unmounted');
});
}
}
7. Vue 3 Computed Properties:
Computed properties allow you to declare a property that is used in the template and is dependent on other properties.
import { computed } from 'vue';
export default {
setup() {
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
return {
firstName,
lastName,
fullName
};
}
}
In the template, you can use {{ fullName }}
and it will always display the current firstName
and lastName
concatenated.
8. Vue 3 Watchers:
Watchers are useful for executing logic when a reactive property changes.
import { watch, ref } from 'vue';
export default {
setup() {
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
return { count };
}
}
This will log a message every time count
is changed.
9. Vue 3 Teleport:
Vue 3 introduces the <teleport>
component that allows you to define a component that is placed in one part of the DOM but is moved to another part of the DOM at render time.
<teleport to="body">
<div v-if="isModalOpen" class="modal">
<!-- modal content -->
</div>
</teleport>
This would render the modal element as a direct child of the body element, even if the teleport component is nested deep within the DOM tree.
10. Vue 3 Suspense:
Vue 3 adds the <suspense>
component that provides a better user experience while waiting for a component to load. It displays some fallback content until the primary content is ready to be rendered.
<suspense>
<template #default>
<AsyncComponent/>
</template>
<template #fallback>
<div>Loading...</div>
</template>
</suspense>
In this example, "Loading..." will be displayed until AsyncComponent
finishes loading.
11. Vue 3 Provide/Inject:
Provide and inject allow an ancestor component to serve as a dependency injector for all its descendants, regardless of how deep the component hierarchy is, as long as they are in the same parent chain.
import { provide, inject } from 'vue';
const ThemeSymbol = Symbol();
// Ancestor component
export default defineComponent({
setup() {
provide(ThemeSymbol, 'dark');
},
});
// Descendant component
export default defineComponent({
setup() {
const theme = inject(ThemeSymbol);
// theme now equals 'dark'
},
});
12. Vue 3 Emitting Custom Events:
In Vue 3, custom events can be emitted using the emit
function, which is provided as the second argument to the setup
function.
export default defineComponent({
setup(props, { emit }) {
const onClick = () => {
emit('my-event', 'payload for the event');
}
return { onClick };
},
});
You can listen to this event in a parent component like so:
<MyComponent @my-event="handleMyEvent"/>
13. Vue 3 Directives:
Let's take a look at a custom directive example in Vue 3. Let's make a directive that changes the color of the text in an element:
import { directive, createApp } from 'vue'
const colorDirective = directive({
beforeMount(el, binding, vnode, prevVnode) {
el.style.color = binding.value;
},
})
createApp(App)
.directive('color', colorDirective)
.mount('#app')
We can use this directive in a component like this:
<div v-color="'red'">This is a red text</div>
14. Vue 3 Plugins:
Creating a plugin in Vue 3 involves exposing an install
function. Here's a basic example:
export default {
install(app, options) {
app.config.globalProperties.$myPlugin = {
// plugin logic goes here
};
},
};
You can then use your plugin like this:
import MyPlugin from './my-plugin';
createApp(App)
.use(MyPlugin, options)
.mount('#app');
15. Vue 3 Router:
Vue Router is the official router for Vue.js. Here's how you define routes and use them in Vue 3:
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/about',
name: 'About',
component: About,
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;
In your main.js:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
16. Vue 3 State Management with Vuex:
Vuex is the official state management library for Vue.js. Here's a basic example:
Install Vuex with npm:
npm install vuex@next --save
Store creation:
import { createStore } from 'vuex'
export default createStore({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
increment(context) {
context.commit('increment')
}
},
modules: {
}
})
Usage in component:
import { computed } from 'vue'
import { useStore } from 'vuex'
export default {
setup() {
const store = useStore()
const count = computed(() => store.state.count)
function increment() {
store.dispatch('increment')
}
return { count, increment }
}
}
17. Vue 3 and Axios:
Axios is often used in Vue applications to make HTTP requests. Here's how you might use it to fetch data from an API:
First, install Axios:
npm install axios
Then, use it in your component:
import { reactive, onMounted } from 'vue';
import axios from 'axios';
export default {
setup() {
const state = reactive({
posts: [],
error: null
});
onMounted(async () => {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
state.posts = response.data;
} catch (error) {
state.error = error;
}
});
return state;
}
}
18. Vue 3 Render Function:
Sometimes, you might need more control over the rendering process. For these cases, Vue provides a render function API:
import { h } from 'vue';
export default {
render() {
return h('div', {}, 'Hello Vue 3 with Render function');
}
}
19. Vue 3 Testing with Vue Test Utils:
Vue Test Utils is the official unit testing utility library for Vue.js. Here's a simple test for a component using Jest and Vue Test Utils:
First, install necessary packages:
npm install --save-dev @vue/test-utils jest
Then, a simple test could look like this:
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
test('increments count when button is clicked', async () => {
const wrapper = mount(Counter)
wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.find('div').text()).toMatch('1')
})
20. Vue 3 Mixins:
Mixins are a way to distribute reusable functionalities for Vue components. Here's how you define and use a mixin:
// Define a mixin object
const myMixin = {
created() {
this.hello()
},
methods: {
hello() {
console.log('hello from mixin!')
}
}
}
// Component that uses the mixin
export default {
mixins: [myMixin]
}
Now, whenever this component is created, it will log 'hello from mixin!'.
21. Vue 3 Slots:
Slots provide a way to define placeholders in a component template that can be filled with content from a parent component. Here's a simple example:
In the child component:
<template>
<div>
<slot></slot>
</div>
</template>
In the parent component:
<template>
<ChildComponent>
<p>This is some original content</p>
</ChildComponent>
</template>
22. Vue 3 Scoped Slots:
Scoped slots are a way to create slots that have access to properties from the child component.
In the child component:
<template>
<div>
<slot name="header" :user="user">
{{ user.firstName }}
</slot>
</div>
</template>
<script>
export default {
data() {
return {
user: {
firstName: 'John',
lastName: 'Doe'
}
}
}
}
</script>
In the parent component:
<template>
<ChildComponent>
<template #header="{ user }">
<p>{{ user.firstName }} {{ user.lastName }}</p>
</template>
</ChildComponent>
</template>
23. Vue 3 Transition & Animation:
Vue provides various ways to apply transition effects to elements when they are added, updated, or removed from the DOM. Here's an example:
<template>
<div>
<button @click="show = !show">
Toggle
</button>
<transition name="fade">
<p v-if="show">Hello</p>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
show: true
}
}
}
</script>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>
24. Vue 3 Custom Events:
Vue 3 components can emit custom events with the emit
method:
export default {
setup(props, { emit }) {
const onClick = () => {
emit('myEvent', 'Hello, Vue 3!')
};
return { onClick };
}
};
In a parent component, you can listen to this event with v-on
or @
:
<ChildComponent @myEvent="handleMyEvent"/>
25. Vue 3 Server-Side Rendering (SSR):
Vue.js also supports building server-rendered applications using vue-server-renderer. However, it's recommended to use Nuxt.js when building server-rendered applications with Vue.js as it abstracts away a lot of the complexities of managing server-rendered state and provides a higher-level, more opinionated framework with conventions.
26. Vue 3 Renderless Components:
A renderless component is a component that doesn't render any of its own HTML but instead provides functionality to other components. This is an advanced technique used to create reusable functions as components:
import { ref } from 'vue';
export default {
name: 'RenderlessCounter',
setup(_, { slots }) {
const count = ref(0);
const increment = () => {
count.value += 1;
};
return () => slots.default({ count: count.value, increment });
}
};
Then you can use this renderless component like so:
<RenderlessCounter v-slot="{ count, increment }">
<button @click="increment">
You clicked me {{ count }} times
</button>
</RenderlessCounter>
27. Vue 3 Composition API vs Options API:
Vue 3 introduced a new way of writing components called the Composition API, which is an alternative to the Options API, used in Vue 2. Here's the same component written with both APIs:
Composition API:
import { ref } from 'vue';
export default {
setup() {
const count = ref(0);
function increment() {
count.value++;
}
return { count, increment };
},
};
Options API:
export default {
data() {
return {
count: 0,
};
},
methods: {
increment() {
this.count++;
},
},
};
28. Vue 3 and TypeScript:
Vue 3 has better TypeScript support than Vue 2. You can write components like this:
import { defineComponent } from 'vue';
export default defineComponent({
name: 'HelloWorld',
props: {
msg: String,
},
setup(props) {
// TypeScript can infer the type of 'props'
console.log(props.msg);
},
});
29. Vue 3 and the Vue CLI:
The Vue CLI is a powerful tool that can help you bootstrap your Vue.js applications. It comes with a graphical user interface, and it's flexible, supporting a variety of configurations:
Install Vue CLI globally with npm:
npm install -g @vue/cli
Create a new project:
vue create my-project
Serve the project:
cd my-project
npm run serve
30. Vue 3 Filters:
Vue 2 filters are removed in Vue 3. The recommended migration strategy is to use methods or computed properties instead:
import { ref, computed } from 'vue';
export default {
setup() {
const message = ref('hello');
const uppercaseMessage = computed(() => message.value.toUpperCase());
return {
message,
uppercaseMessage,
};
},
};
31. Vue 3 Functional Components:
Functional components are a type of component that doesn't have any state and consists purely of a render function. Functional components in Vue 3 are written like this:
export default {
functional: true,
render() {
// component logic
}
}
32. Vue 3 Custom Composition Functions:
With the Composition API, you can create custom composition functions. Here's an example of a custom hook that fetches data from an API:
import { ref, onMounted } from 'vue';
import axios from 'axios';
export function useFetchData(url) {
const data = ref(null);
const isLoading = ref(true);
onMounted(async () => {
try {
const response = await axios.get(url);
data.value = response.data;
} catch (error) {
console.error(error);
} finally {
isLoading.value = false;
}
});
return { data, isLoading };
}
Then you can use this function in a component:
import { useFetchData } from './useFetchData';
export default {
setup() {
const { data, isLoading } = useFetchData('/api/data');
return { data, isLoading };
}
};
33. Vue 3 Teleport:
The <teleport>
component lets you control where your component's template is rendered in the DOM. It's great for modals, pop-ups, and other UI elements that need to break out of their container. Here's an example of how to use it:
<teleport to="#end-of-body">
<div class="modal">
This will be rendered at the end of the body!
</div>
</teleport>
34. Vue 3 Suspense:
The <Suspense>
component lets you "wait" for some condition and display some fallback content while waiting. It's mostly used with async components. Here's an example:
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
35. Vue 3 Reactivity with Proxies:
Vue 3 uses JavaScript Proxies for its reactivity system, which allows it to track changes to objects and arrays better than Vue 2. This is a low-level detail, and you normally won't have to worry about it, but it's part of what makes Vue 3 more powerful and flexible than Vue 2.
The teaching could go on indefinitely as Vue is a broad and deep framework that offers a multitude of functionalities and patterns. But hopefully, this guide has given you a solid foundation on which to build your Vue knowledge. Remember to practice what you've learned, as practical application is key to solidifying these concepts! Let me know if you have more questions or need information on other topics.
36. Pinia
Pinia is an alternative state management library for Vue.js. It aims to provide a simpler and more straightforward API than Vuex, and it's fully compatible with the Vue 3 Composition API.
First, let's install Pinia:
npm install pinia
Creating a store:
In Pinia, instead of defining one big Vuex store, you define multiple smaller stores. Here's an example of a store:
import { defineStore } from 'pinia';
export const useCounterStore = defineStore({
id: 'counter',
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++;
},
reset() {
this.count = 0;
}
},
});
Accessing the store:
In a Vue component, you can access the store like this:
import { useCounterStore } from './stores/counter';
export default {
setup() {
const counter = useCounterStore();
return {
count: counter.count,
increment: counter.increment,
reset: counter.reset,
};
},
};
Using the store in the template:
And you can use the store in your template like this:
<template>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
<button @click="reset">Reset</button>
</template>
Setting up Pinia:
You also need to install Pinia in your application:
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
app.use(createPinia());
app.mount('#app');
That's the basics of Pinia! It provides a simpler API and is more focused on the Composition API, making it a great choice for Vue 3 applications. You can create more complex stores with actions that commit mutations and get state from other stores, similar to Vuex.
37. Fetching data within a Pinia store:
Pinia stores can handle async actions, which makes them great for fetching data:
import { defineStore } from 'pinia';
import axios from 'axios';
export const useUserStore = defineStore({
id: 'user',
state: () => ({
user: null,
}),
actions: {
async fetchUser(id) {
const response = await axios.get(`/api/users/${id}`);
this.user = response.data;
},
},
});
38. Computed properties in Pinia store:
Pinia also supports computed properties in the store. Let's modify the previous example to include a computed full name:
import { defineStore } from 'pinia';
import axios from 'axios';
export const useUserStore = defineStore({
id: 'user',
state: () => ({
user: null,
}),
actions: {
async fetchUser(id) {
const response = await axios.get(`/api/users/${id}`);
this.user = response.data;
},
},
getters: {
fullName() {
return this.user ? `${this.user.firstName} ${this.user.lastName}` : '';
},
},
});
39. Accessing one store from another:
Sometimes, you need to access data from one store inside another store. You can do this by importing the other store inside the actions or getters:
import { defineStore } from 'pinia';
import { useUserStore } from './user';
export const usePostsStore = defineStore({
id: 'posts',
state: () => ({
posts: [],
}),
actions: {
userPosts() {
const userStore = useUserStore();
return this.posts.filter((post) => post.userId === userStore.user.id);
},
},
});
40. Testing Pinia stores:
Testing Pinia stores is straightforward because you can import the store functions directly into your test and call their actions and getters:
import { useCounterStore } from './counter';
test('increment increases count', () => {
const counter = useCounterStore();
counter.increment();
expect(counter.count).toBe(1);
});
41. Server-side rendering (SSR) with Pinia:
Pinia supports server-side rendering (SSR) out of the box. It can serialize the state of your stores and then deserialize it on the client side, so the client starts with the same state as the server.
Pinia provides a more idiomatic way to handle state in Vue 3 with the Composition API, and it's rapidly gaining popularity. Its flexible and modular nature allows for a cleaner and more maintainable codebase for complex applications. As always, feel free to ask if you have any questions or need more information on a specific topic!
42. Resetting a store:
To reset a store in Pinia, you can define a reset action in your store:
import { defineStore } from 'pinia';
export const useCounterStore = defineStore({
id: 'counter',
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++;
},
reset() {
// Resets the state to its initial state
Object.assign(this.$state, this.$initState);
},
},
});
The $initState
property holds a copy of the initial state, and Object.assign
is used to replace the current state with the initial state.
43. Handling errors in a Pinia store:
When handling asynchronous actions in Pinia, you'll want to have a way to handle errors. You can do this with try/catch:
import { defineStore } from 'pinia';
import axios from 'axios';
export const useUserStore = defineStore({
id: 'user',
state: () => ({
user: null,
error: null,
}),
actions: {
async fetchUser(id) {
try {
const response = await axios.get(`/api/users/${id}`);
this.user = response.data;
} catch (error) {
this.error = error;
}
},
},
});
44. Loading state in a Pinia store:
Often, you'll want to display a loading spinner while fetching data. You can add a loading
state to your store:
import { defineStore } from 'pinia';
import axios from 'axios';
export const useUserStore = defineStore({
id: 'user',
state: () => ({
user: null,
loading: false,
error: null,
}),
actions: {
async fetchUser(id) {
this.loading = true;
try {
const response = await axios.get(`/api/users/${id}`);
this.user = response.data;
} catch (error) {
this.error = error;
} finally {
this.loading = false;
}
},
},
});
45. Reusing logic across stores:
Since Pinia stores are just JavaScript functions, you can extract common logic into separate functions and reuse them across stores. For instance, if multiple stores have a loading
state, you could create a useLoading
function:
export function useLoading() {
return {
loading: false,
startLoading() {
this.loading = true;
},
stopLoading() {
this.loading = false;
},
};
}
Then use it in a store:
import { defineStore } from 'pinia';
import { useLoading } from './useLoading';
import axios from 'axios';
export const useUserStore = defineStore({
id: 'user',
state: () => ({
...useLoading(),
user: null,
error: null,
}),
actions: {
async fetchUser(id) {
this.startLoading();
try {
const response = await axios.get(`/api/users/${id}`);
this.user = response.data;
} catch (error) {
this.error = error;
} finally {
this.stopLoading();
}
},
},
});
46. Mocking external dependencies in tests:
When testing Pinia stores, you may need to mock external dependencies, like API calls. You can use libraries like jest.mock to achieve this. Here's an example:
import { useUserStore } from './user';
import axios from 'axios';
jest.mock('axios');
test('fetchUser fetches a user', async () => {
const user = { id: 1, name: 'John Doe' };
axios.get.mockResolvedValue({ data: user });
const userStore = useUserStore();
await userStore.fetchUser(user.id);
expect(userStore.user).toEqual(user);
});
And there you have it! We've journeyed together through the fundamentals of Vue 3 and explored the intriguing world of Pinia. We've unpacked the power of the Composition API, delved into creating dynamic components, and understood how to manage state using Pinia. This guide provides a solid foundation, but remember that the world of web development is vast and continually evolving.
To deepen your Vue 3 and Pinia knowledge, I encourage you to build your own projects and experiment with these concepts. There's no better way to learn than by doing.
For more in-depth exploration, the official Vue 3 documentation is an invaluable resource and is filled with comprehensive guides, examples, and tips. The Vue community is vibrant and active, with plenty of tutorials, articles, and forums available. The official Pinia documentation is also a great starting point for understanding advanced state management in Vue 3.
Participate in code challenges and contribute to open-source projects. Both will expose you to different coding styles and real-world scenarios.
Remember, the journey of learning never ends and every step you take is progress. Keep exploring, keep experimenting, and most importantly, enjoy the process! Happy cooking errr.. coding!! :)
If you enjoy my technology-focused articles and insights and wish to support my work, feel free to visit my Ko-fi page at https://ko-fi.com/philipjohnbasile. Every coffee you buy me helps keep the tech wisdom flowing and allows me to continue sharing valuable content with our community. Your support is greatly appreciated!
Top comments (1)
Hi @philipjohnbasile,
thank you for this complete article.
One remark though : are you sure about the #45 "Reusing logic across stores" ?
Because as you did it, we will end up with functions inside the state object...
I tested it, it "works" (meaning there are no errors) but in the philosophy of Pinia, state should only be composed with writable objects.
Here is the part of the doc that talks about that : pinia.vuejs.org/cookbook/composabl...
We should better use a "setup store" here to correctly create state and actions properties from the
useLoading
composable.