Whatās up, #VueFriends? Itās time for another Vue article. Today we will put into practice some vue design patterns dispersed on the Internet, but in this case, we pick some of the most useful to apply together with our beloved and great framework. Letās go!
Table of contents
Why do we need design patterns?
A design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design. Thatās the purpose of design patterns, maybe you may find the solution to a given problem on your own, but today I want to show you some easier ways in Vue š.
Builder Pattern
If exists something cannot be missing in a web app, it is the forms, forms everywhere! Forms are boring and more if there is no way to generate them programmatically. Itās a common mistake to write files/components like UserForm.vue
, ContactForm.vue
, LoginForm.vue
, and others with a similar logic inside.
Thatās OK if your application has few forms, but what happens if your application has more than 20 or 100 forms? šµ. With some time, even naming them tends to get harder and harder. Think about the above scenario. How times do you write the <input>
element, validations, styles, and bindings? So, donāt pray more š here is the Builder Pattern!
Builder is a creational design pattern that allows you to build complex object step by step. These patterns produce different types and representations of an object using the same construction code. Think about our case: we need to construct a form builder. Forms have many fields and submit button (maybe a cancel button too). One field means one kind of input, label, and error message. All fields have this structure and have an order.
First, we need to create the FormBuilder
object as follows.
1 | import { defineComponent, h, type VNode } from "vue"; |
We need some structure to put all the fields in order. An array is enough (we are using Typescript, this approach adds more code). The methodĀ addField
,Ā as its name says, only has the task of putting an object Field to the fields array. The magic happens in theĀ build
Ā method. It defines a new Vue instance with all our fields well structured.
Up to here, what we want is similar to the following schematic image.
Letās continue, What is theĀ FormFactory
Ā task? Itās responsible for structuring the form (many kinds of forms). Here is the code:
1 | <template> |
Maybe it isnāt the ideal Form Builder, but itās useful. The more complex your form type, the more complex your form builder will be. Now you have a Form Builder, which you can use to create forms in each view, but we have one step more, theĀ FormDirector
.Ā It has the task of structuring our forms in a single file with verbose methods, likeĀ makeLoginForm()
,Ā makeSignUpForm()
,Ā makeShopForm()
, and more. Here is ourĀ FormDirector.ts
.
1 | import VInput from "@/components/form/VInput.vue"; |
To validate our forms, we are using theĀ zodĀ library. As we can see,Ā makeLoginForm()
Ā uses the Form Builder class to create a new app form with a detailed description of each field.
Code: Ā vue design pattern repository, execute, and go toĀ /builder-patternĀ route š.
Adapter Pattern
The adapter pattern is a structural design pattern also known asĀ āWrapperā.Ā This pattern allows the interfacing of an existing class used as another interface, like something in the middle of a connection. The perfect real-world example is when you go to another country, you realize that the power plug is different. So, you will need an adapter to get power. Thatās the magic.
A typical use case is to wrap a 3rd party library, which is used everywhere in your code. Someday, many libraries will be deprecated, or in the worst case, security issues, also known as vulnerabilities. To learn more about it, we have an amazing academy,Ā https://websecurity-academy.com/, and a free guide related toĀ Vue Security Best Practices.
In this case, we are using theĀ js-cookieĀ library to interact directly with our Cookies, which is simple and lightweight. The adapter looks like this.
1 | import Cookies from "js-cookie"; |
If one day we will need to replace the 3rd party library or add more features, it will be fast, and many parts of your code wonāt be affected by this change, as we can see in the following schematic image.
Letās think about another situation: You have an extensive web application with dozens of elements from some UI material library/framework (like Vuetify). Tomorrow, the business requirements change to adopt Quasar because of the long-term support. You have picked some elements from Vuetify, and now itās time to change it šØ. But this scenario happens and could be acceptable, but as time goes on, technologies improve, and again a change is needed. What tedious work. The solution? Create your adapter inputš.
Container/Presentational Pattern
Maybe you are using this design pattern right now, itās simple, and thatās the point. Vue and other modern frameworks have two parts where you can put the code, the presentational layer and the business logic layer. Those are known as concerns, and generally isnāt convenient to mix them up, but it doesnāt mean that one canāt use the other, and this is where Container or Presentational Pattern lands.
Itās time to imagine another scenario: You need a list of To-Do items. The easy way is to create a Vue SFC (Single File Component) to show all of them, something like this.
1 | <template> |
It works well, but what happens if we need to test this component all in one file the business logic layer (call to an API) is mixed with the presentational layer, so there is no way to reuse something and extract to test, sooner than later this type of code will need a refactoring. So, the Container Pattern helps us to decouple the presentational and business logic layer and turns to a testable application.
To take this pattern advantage, think of this file as three parts:
- Presentational Component, this component only has the task of receiving data through props, event bus, injection, state management system, or another to present this data visually.
- Business Logic Component/fileĀ has the task of fetching the data from somewhere (APIs commonly) to share with any component or file which needs it. It acts like an action administrator of all the actions registered on the presentational component.
- Container ComponentĀ handles the interaction of the presentational component and the business logic component. In our case, this component calls some method to get the To-Dosā information and will pass that data on to the presentational component as props.
So, itās time to implement the code, first theĀ Business Logic Component:
1 | import type { StoreOptions } from "vuex"; |
TheĀ todo.ts
Ā is a Vuex store definition with an API call inside. Disponible to any component which needs the list of to-dos.
TheĀ Presentational ComponentĀ calledĀ VList.vue
Ā looks like this.
1 | <template> |
No matter the source, the data enters through props to display a list of beautiful To-Dos āØ.
Finally, the Container Component has the principal mission of calling the data from the Business Logic Component and passing the to-do data to display to the Presentational Component. Here is the code.
1 | <template> |
Code: Ā vue design pattern repository, execute, and go toĀ /container-patternĀ route š.
Provide/Inject Pattern
There is another interesting design pattern. When we start with Vue, most of us make the mistake of passing information through props between multiple components, as the image shows.
Ideally, theĀ OptionList
Ā component should only renderĀ OptionItem
, but with this approach, it containsĀ visualizationPreferences
Ā as one of its props. It can still work without much trouble, but what if you must go through many components to get to the component that will use that property? Many of those components donāt need a new prop, only to fit with one component at the bottom of the hierarchy (a child component).
That problem is known asĀ props drillingĀ (an anti-pattern). Fortunately, Vue has the solution build-in with itsĀ provide()
Ā andĀ inject()
Ā features, better known as dependency injection.
As we can see in the image above,Ā ProvideĀ gets the data and is responsible for passing it when it is needed to beĀ InjectedĀ in all the components down the hierarchy (in this caseĀ OptionItem).
The following case shows a list of cards (with a title and image inside). We need to change the card appearance usingĀ provide()
Ā andĀ inject()
. Those cards have four styles: rounded, squared borders, and dark and light themes. This pattern is ideal for implementing more cards along the same behavior.
So, to implement this feature, we need to create the image structure in the code, beginning withĀ OptionItem.vue
.
1 | <template> |
OptionItem.vue
Ā pretends to be a card with a title and a random image inside.
TheĀ setup()
Ā uses theĀ inject()
Ā method to retrieve (inject) the data from theĀ provider()
Ā method, to be used inĀ OptionContainer.vue
Ā (two components above in the hierarchy).Ā
To inject that data inside the component, we need a key. In this case,Ā VISUALIZATION_PREFERENCES
Ā is anĀ InjectionKey
.Ā Its use makes sense with Typescript. It allows you to define a key name and structure for a specific data structure. TheĀ VISUALIZATION_PREFERENCES
Ā is in theĀ OptionSymbols.ts
Ā file for re-usability purposes.
1 | import type { InjectionKey } from "vue"; |
The next component implemented isĀ OptionList.vue
, which works as a Presentational Component (only renders everyĀ OptionItemĀ component if it has the necessary data, it is only a dummy component).
1 | <template> |
Now, theĀ OptionContainer.vue
Ā is the Provider. It contains the data to pass toĀ OptionItem.vue
Ā through theĀ provide()
Ā method, as we can see below.
1 | <script lang="ts"> |
The code is large (this is the script part only). TheĀ provide()
Ā method receives the sameĀ VISUALIZATION_PREFERENCES
Ā seen previously as the first parameter, and the data to be injected as a second parameter (we are usingĀ computed()
Ā to turn reactive that data).
Many people use this pattern without the reactive feature, but it is great to have.
Code: Ā vue design pattern repository, execute, and go toĀ /provide-inject-patternĀ route š.
Composables
If you are coming from React, this reactive pattern could be familiar. Yes, we are talking aboutĀ hooks, but in Vue, they are called āComposablesā; hooks and composables are component patterns. According to theĀ official documentationĀ of Vue, a ācomposableā is a function that leverages Vueās Composition API to encapsulate and reuse stateful logic. Stateful logic involves a managing state that changes over time.
Letās take advantage of the previous example explained in the Adapter Pattern. We have the classĀ CookiesAdapter
Ā which wraps theĀ js-cookie library. CookiesAdapter
could be used directly in our code, but now we will add state and covert to a Composable, to see the cookiesā body reactively.
1 | import { ref, type Ref } from "vue"; |
TheĀ ref()
Ā methodĀ is a function that gives reactivity to a variable,Ā cookies
.Ā In this case, when we talk about composables, we are directly talking about state and action exposure. This composable exposes three actions:Ā get()
,Ā set()
, andĀ remove()
; those do the same thing as theĀ CookiesAdapter
Ā methods but are necessary to share through components as a composable, the same withĀ cookies
Ā which is the general state of our Cookies. The trick behind theĀ update()
Ā method synchronizes theĀ cookiesāĀ state every time an action is called.
Now, we can use it everywhere.
1 | <template> |
Many people use this pattern without the reactive feature, but it is great to have.
This cookie composable is cool, but if you donāt want to reinvent the wheel, the Vue ecosystem has many composition functions on the hand of VueUse and vue-composable libraries.
Code: Ā vue design pattern repository, execute, and go toĀ /composable-patternĀ route š.
State Management Pattern
State management is a crucial part of our web apps. In Vue, we have two great libraries to handle the state. To explain those libraries, we have the following case: We want to show to-do items in a list, to-do items retrieved from an open API (https://jsonplaceholder.typicode.com/todos). Similar to the case exposed in the Container Pattern section, we will reuse theĀ VList.vue
Ā component. Now, we only need to think about how to use both state management libraries.
Vuex/Pinia
Accordingly, with the officialĀ documentationĀ of Vuex, it serves as a centralized store for all component in an application, with rules ensuring the state, which can be mutated predictably. The following code shows a Vuex store configured to manage to-dos.
1 | import type { StoreOptions } from "vuex"; |
In contrast, we have the same implementation but powered by Pinia.
1 | import type { Todo } from "./../types/todo"; |
The unique difference is that Pinia doesnāt need to define mutations. We need a new component and use the to-do store implemented above with Vuex.
1 | <template> |
The Pinia version looks like this.
1 | <template> |
Both work well, but the superficial differences are few. But Pinia hides something really interesting. We are using Pinia as a Composable. Remember, the principal mission of this pattern is to manage and expose the state in a good way. Naturally, Pinia does that and fits in. but the Pinia approach has another advantage, Type friendly. We donāt need to point specific stores, actions, mutations, getters, or others via strings (a non-type friend); it happens in Vuex. In the past, that detail always caused me problems with the code. Instead, in Pinia, each state, action, or getter will be accessible directly from the source as aĀ useStoreSomething()
.Ā Pinia is the new by-default state management library for Vue. We recommend using it belong the composition API.
Code: Ā vue design pattern repository, execute, and go toĀ /store-management-patternĀ route š.
Summary
Writing this article was a big challenge for me. I hope these examples and guideline helps you to make the best architectural decisions. Getting motivated and up to date is hard. If you found it useful, donāt doubt to share this article with your #VueFriends.
I donāt want to say goodbye without first recommending the Lachlan MillerāsĀ bookĀ called āDesign Pattern for Vue.js. A Test-driven Approach to maintainable Applicationsā which inspired me to write this article and the magnificentĀ conferenceĀ of Jacob Schatz (you will find more advanced patterns with Vue).
As we mentioned earlier, all the code is availableĀ here. Clone, execute, and enjoy it. Says goodbye, your #VueFriendĀ @cr0wg4n.
Comments