Styling Vue Single-File Components
Nov 4, 2021
Building Web Components with Vue 3.2
Oct 5, 2021
Vue 3.2 - Using Composition API with Script Setup
Aug 26, 2021
Aug 18, 2021
Progressive Web Apps and Mobile Apps
Jul 22, 2021
Getting Started with Tailwind in Vue
Introduction Tailwind CSS has taken the front end development world by storm. If you haven't heard of it yet, Tailwind describes itself as, "a utility-first CSS framework packed with classes like flex, pt-4, text-center, and rotate-90, that can be composed to build any design, directly in your markup." Rather than using semantic class names (like "button" or "title"), you utilize these smaller utility classes to build your components. There are many ways to integrate Tailwind within a Vue application. In this article, we'll be talking about a few of the best practices for installing and utilizing Tailwind in a Vue application. Vue CLI Installing Tailwind in a Vue CLI app is probably the easiest experience available. There is a CLI plugin that does everything we need, including installing Tailwind, and configuring PostCSS for us. Even better, it works for both Vue 2 and 3! In your terminal, at the root of your application, run the following command: ` The CLI will then install the plugin. You will be asked how complete a Tailwind configuration file you want (none, minimal, or full). Choose "minimal" for now - it will create the file, but will not populate it with any custom values. Once the terminal has finished, you're done! There are a couple considerations to keep in mind when using this approach: - As of this writing, the CLI plugin has not been updated in a few months. It is currently installing Tailwind version 2.0.2 (the current latest is 2.2.4). This is easy enough to adjust manually, but you need to be aware of it to make the change yourself. - The plugin is also providing the PostCSS 7 compatible build. For the most part, this doesn't make a huge difference. The Tailwind docs notes that the compatibility build is identical with the main build, so you won't be missing out on any features. For now, this should be fine, but it could lead to issues down the road if you want to use PostCSS 8 plugins (or if Tailwind decides to stop providing backwards-compatible builds). If you're using Vue 3.0.6 or greater, you should be able to upgrade to the main builds. However, your should make sure to update all your PostCSS plugins to the latest main builds, not just Tailwind. Vite The Tailwind Docs include instructions on installing Tailwind with Vite. While it isn't as straightforward as the Vue CLI plugin, it's still pretty simple to get started in Vite. First, install Tailwind, PostCSS 8, and Autoprefixer in your repository: ` Then, run the Tailwind CLI command to create a default configuration file for both Tailwind and PostCSS: ` This will create two files: - tailwind.config.js - postcss.config.js Unless you want to make a change to either Tailwind or PostCSS, you won't have to touch these files. The docs recommend that you configure the purge option, which is used by Tailwind to purge its unused styles. With this change, your tailwind.config.js file should look like this: ` Note that we also added the mode: 'jit' key to the configuration. This enables Tailwind's Just-In-Time mode, which is a more performant experience, and enables a number of other Tailwind features. Read the documentation to learn more, and better understand what you're getting out of this. Now, let's create a base CSS file to import Tailwind's classes into. The Tailwind docs suggest using ./src/index.css, but I would recommend creating a separate CSS file just for Tailwind. Let's call it tailwind.css. In that file, put the following: ` This uses PostCSS to import the various classes and utilities that Tailwind uses. Then, we just need to import this file in our main.js file like this: ` And we're done! Tailwind is now available in your Vite application. Make sure to clear out the index.css file of any styles you do not want. In practice, that file will probably be much smaller than you may be used to, but it's still useful to keep your custom styles separate from Tailwind. Nuxt Nuxt has an amazing plugin ecosystem, and there is a Tailwind plugin available for us to use here as well. Unlike with Vue CLI, there's a bit more manual work involved to install a Nuxt plugin, so let's get started! First, make sure your Nuxt application is at least version 2.15.3. This is the release of Nuxt that supports PostCSS 8. Unlike Vue CLI, this plugin provides the main build of Tailwind, not the compatibility build for PostCSS 7. You can upgrade Nuxt by running npm update nuxt. Once you have confirmed that your Nuxt app is ready, run the following command in your terminal: ` This will install Tailwind and its required plugins for you. Then, in your nuxt.config.js, add the plugin to your buildModules: ` Finally, run npx tailwindcss init to create the Tailwind configuration file. And that's it! Tailwind is now ready to go in your Nuxt application. You may notice that there's a lot less work here than in Vite (and a lot fewer files being created than in Vue CLI). With the Nuxt plugin, if you do not have a tailwind.css file where styles are being injected, Nuxt will create it for you (the same is true for the tailwind.config.js file, but typically you'll want that anyway to enable JIT mode, or other custom configurations). Tailwind Viewer One benefit of the Nuxt plugin is the Tailwind Viewer. This allows you to previous the styles currently enabled in your Tailwind configuration, and copy/paste the classes into your UI. By itself this is a great feature, but when you start working with custom colors or other features, it can be invaluable to preview them before adding the classes to your template. Conclusion We've walked through adding Tailwind to a Vue application in three of the most common tools to build Vue applications. In general, the process is the same: 1. Install Tailwind, PostCSS, and Autoprefixer 2. Configure Tailwind and PostCSS 3. Import a CSS file that includes the Tailwind classes to your application If you are using a custom setup for your Vue site, you should be able to apply these steps to your own setup in order to get Tailwind up and running. There are also a number of resources available if you run into any problems, including a Discord server and GitHub Discussions, where you can ask for help from other Tailwind users. A note on utility-first CSS As I'm writing this, I'm aware that utility-first CSS is not for everyone. If that sounds like you, and you made it this far, one thing about Tailwind that isn't highlighted as much as it should be is how the configuration allows for building a coherent design system. How often do you see multiple configuration files for Sass that are nothing but variables ($primary, $red, etc.)? All of this can be handled within Tailwind's configuration file, with the added benefit of generating classes for text, backgrounds, borders, and more to utilize those colors. Also, Tailwind already comes built-in with a number of great design decisions for padding and margin, flexbox, and more, making the work of building a design system that much easier. Also, Tailwind provides a special directive, @apply, that lets you utilize Tailwind classes in standard CSS. This means that, rather than using the utility classes Tailwind provides in your template, you can build out custom classes (like .card or .button) while still benefitting from Tailwind's other features. Even better, using a plugin like PostCSS Nested, you can get nested CSS, just like in Sass or other CSS preprocessors. Conisder the following component: ` This is a fairly simple button, with a background color, padding on width and height, and a hover attribute. Already, we can see a number of other changes to make (add some drop shadow, transitions, different styles for disabled state, and so on). Using purely Tailwind, that could lead to a large number of classes on the button element. Let's say you don't like having those large class attributes, but you still want the other benefits of Tailwind. Here's the same example, using @apply to build a custom .button class: ` Nice! Our template is a lot simpler, and the styles are contained in our style block. That said, most Tailwind users will probably want to go without using @apply (Adam Wathan, the creator of Tailwind, has said that he almost never uses @apply). That doesn't make this any less valid, and is a perfect way to balance to strengths of Tailwind with traditional methodologies of writing CSS like BEM. If you're hesitant to try Tailwind because of its large number of classes, give this a try and see how you like it! And remember, the goal of using Tailwind or any other CSS framework or methodology is to build a stellar experience for your application's users. Don't lose focus of that while deciding whether to adopt Tailwind in your latest Vue application. Try it out, see if it works for you, and decide for yourself whether it fits your project or style of writing CSS. Until next time!...
Jul 19, 2021
Vue 3.1 - Official Migration Build from Vue 2 to 3
Introduction A few weeks ago, the Vue core team released the first minor version of Vue 3 (release notes here). This latest release of Vue brought a number of fixes and changes, but most importantly, introduced a migration path from Vue 2 to Vue 3. Bilal Haidar previously wrote about how to upgrade a Vue 2 application to Vue 3. But at this point, the Vue core team has provided a straightforward method to upgrade an existing application. In this post, we'll explore the path to upgrade an existing Vue 2 application, as well as when to hold off on your upgrade. We will be referencing the official Migration documentation as we go through this blog post. What is the Migration Build? From the Vue documentation: > @vue/compat (aka "the migration build") is a build of Vue 3 that provides configurable Vue 2 compatible behavior. > The migration build runs in Vue 2 mode by default - most public APIs behave exactly like Vue 2, with only a few exceptions. Usage of features that have changed or have been deprecated in Vue 3 will emit runtime warnings. A feature's compatibility can also be enabled/disabled on a per-component basis. What does this mean for us? In general, it means that components and application features that were written in Vue 2 will be made compatible in Vue 3. This incldues breaking changes such as v-model, which had a syntactic change in Vue 3. The intent of the migration build is to provide a way to shift your version of Vue from 2.6 to 3.1, without having to rewrite every component before it works. During the upgrade, you will be able to start up your application and see a list of warnings reported in the browser console. These warnings reflect the various API changes between Vue 2 and 3, and are meant to guide you through the upgrade process. If you were to upgrade without the migration build, your app would crash on the first error, leading to a very frustrating experience. The migration build is _not_ intended to be used long-term. It is provided as a way to upgrade your codebase from Vue 2 to Vue 3, not as a way to continue using deprecated APIs. If your application is reliant on Vue 2 for a library or deprecated API, I would recommend holding off until Vue 2.7 is released (which will backport some new Vue 3 features such as the composition API). That said, you absolutely can deploy your Vue 3 application with the migration build if you have to. From the documentation: "If you do get your app running on the migration build, you can ship it to production before the migration is complete. Although there is a small performance/size overhead, it should not noticeably affect production UX." Keep in mind that this is not the intent of the migration build, and plans should be made to migrate completely as soon as possible rather than rely on the migration build to keep old code working as expected. Ready to Upgrade? We will be using this repository as an example for our Vue 3 migration. It is a Vue CLI application that implements a few components: v-model, Vuex, and Vue Router. Note that we aren't using any major third-party libraries like Vuetify or Nuxt. Why does that matter? Per the documentation: > Dependencies that rely on Vue 2 internal APIs or undocumented behavior. The most common case is usage of private properties on VNodes. If your project relies on component libraries like Vuetify, Quasar or ElementUI, it is best to wait for their Vue 3 compatible versions. > The migration build can be used for [server-side rendering], but migrating a custom SSR setup is much more involved. The general idea is replacing vue-server-renderer with @vue/server-renderer. Vue 3 no longer provides a bundle renderer and it is recommended to use Vue 3 SSR with Vite. If you are using Nuxt.js, it is probably better to wait for Nuxt 3. Because these large frameworks rely on a great deal of internal APIs or custom configuration, we will have to wait until they release their own compatible builds for Vue 3. Luckily, each of these has new builds under development already. - Vuetify has released an alpha build of its component library. I would expect a final build sometime this year. - Quasar has already previewed a release candidate, and should be ready to release in the near future (at the time of writing). - Nuxt has not announced anything publicly, but in a conversation with Daniel Roe from the framework team, private alpha builds have been in testing since Q1 of 2021, with public builds expected by the end of June/start of July. A final build is expected by the end of 2021. Keep in mind that the Vue documentation is only referencing the larger libraries and frameworks. There may be a library that you use in your project that is incompatible with Vue 3, and could cause errors even with the migration build. Upgrading to Vue 3 is a major change, and issues should be expected. Also, there are some applications that simply won't be able to migrate, even with the migration build. Some larger applications could simply be too much effort to migrate to Vue 3, or rely on certain undocumented features that are not available in Vue 3. Per the documentation, "If your application is large and complex, migration will likely be a challenge even with the migration build. If your app is unfortunately not suitable for upgrade, do note that we are planning to backport Composition API and some other Vue 3 features to the 2.7 release." Perform Upgrade To perform the upgrade, we will be following the same steps as noted in the documentation. For our purposes, we will be using the Vue CLI instructions. But please refer to the documentation for using a custom Webpack build or Vite with Vue 2. Upgrade dependencies The first step we need to take is upgrade our tooling. Since we are using Vue CLI, we can simply run vue upgrade in the terminal. If you have any Vue CLI dependencies that need to be updated, you will see a screen like the below: Proceed with the upgrade for each of the plugins. You may be asked for additional input depending on which plugins you have installed. In my case, I was not prompted for any additional input. Next, we need to update the version of Vue that we're using as well as install the migration build. Run the following commands in your terminal: ` With these commands, we are installing Vue 3.1, the migration build, and the single-file component compiler for Vue 3. We then uninstall vue-template-compiler, which was the SFC compiler for Vue 2. You can also make these changes manually in package.json if you prefer. With that, our dependencies have been updated. Consider making a commit at this point so you don't have to start from scratch if something goes wrong. Remember, this is a migration - ensure your work is being backed up just as if you were upgrading your operating system! Configure compatibility mode in Webpack Now that we have our dependencies, let's enable the migration build. To do that, open (or create) your vue.config.js file at the root of your project, and add the following code: ` What does this do? Rather than use Vue 3 itself (which would throw errors), we are configuring Webpack to use the migration build instead. We then add some configuration to vue-loader (which is used by Webpack to bundle Vue components) to return a compatConfig object. This object allows us to determine which features are enabled or disabled, and whether we are running in Vue 2 or Vue 3 mode. This configuration is available at both the global and component levels. In this instance, we are simply setting our application to Vue 2 mode, and leaving all compatibility features enabled. Start up your application At this point, we should be able to start our newly-migrated Vue 3 application, but we are far from done. Start up your Vue application (npm run serve for a Vue CLI app) and see what happens. You may get a number of compile-time errors (features that are completely incompatible with Vue 3, such as filters, could cause errors at this step). Fix those, then your application should render and act properly in the browser. If you open your browser tools, you will probably notice a number of warnings in the console. Some of them will be related to your own code, others may be dependencies that you're using. Here's an example from the test runner app: For this example, I'm not using Vue.set(), or destroyed, or most of these other warnings being reported. That's because they all come from dependencies that I'm using (in this case, Vue Router). At this point, let's upgrade our dependencies to use the latest version of the libraries, which should resolve the errors. Upgrade dependencies Let's start with upgrading Vue Router. Run the following command in your terminal: ` We'll first upgrade our router. For this application, it's pretty straightforward. If you have a more complex router configuration, here's the documentation on upgrading Vue Router. Below is our updated configuration (removed code is commented out): ` Let's go through the changes. 1. Rather than import Vue and VueRouter into the file, we use the functions createRouter and createWebHashHistory. The first is what generates the router (rather than invoking new VueRouter()), while the second defines the mode (history replaces the mode option in the previous version, and is now required). 2. Because plugins are no longer registered with Vue.use(), this line is removed. 3. We create the router using createRouter() instead of new VueRouter(), including the routes and the required history key. Now let's update our Vuex store. In this code example, we're using a module, which does not have to be changed (how Vuex works has not changed). We only need to update how our Vuex store is initialized. ` The same concepts we used in the router are applied here. Rather than invoking new Vuex(), we use the new function createStore(). We also don't need to register Vuex, because that's not how plugins are registered in Vue 3. Finally, let's update our initialization of Vue. Open main.js, and make the following changes: ` As with the router and Vuex, we no longer use new Vue(), and instead use createApp(). We then chain .use() after that function call to include the router and store (and any other plugins you may have), rather than supplying them as keys in an object. Finally, we call .mount('#app') to render the application in the browser. If we restart our application at this point, we should have cleared out most, if not all, of the errors we were seeing previously. Go through the remaining errors you see, and try to resolve them. Any components with errors should be properly reporting such in the terminal (see below screenshot). While writing this article, I noticed that the Vue Router was still causing at least one warning to be thrown. Make sure you read the warning message before you start to worry too much, it could just be an alert that there is a behavior change. Compat Configuration During this step, you can configure the compatConfig at either the global or component level. For example, if you have components that are using the destroyed lifecycle hook (instead of Vue 3's unmounted), and you really want to disable that feature, you can make the following configuration change: ` Using this configuration, the destroyed lifecycle hook will not be called. In this case, Vue will continue to report any instances of the destroyed hook in your code, and note that not fixing it could cause runtime errors. Other warnings, such as changes to how watch functions, can simply be disabled if you understand the change, and the risks involved. Remove Compat Build Now that we've finished the upgrade, it should be safe to remove the migration build (remember, it's not meant to be included forever!). Run the following command: ` Then, make sure you undo your changes in vue.config.js. Since we aren't using the compatibility build any more, the alias can be removed, and the vue-loader config can be completely removed. Try running your application again, and make sure you don't have any lingering errors. If not, congratulations! You have successfully migrated to Vue 3! Conclusion In this article, we have taken a fairly simple application through the migration process from Vue 2 to 3. Depending on the complexity of your application, you may run into more incompatible features that need to be adjusted, or you may be blocked altogether due to a library/framework you rely on not being ready for Vue 3 yet. Remember that the ecosystem is still working towards adopting Vue 3 as the default, and there is still some way to go before everything will work as we expect. Also, keep in mind that if your application won't be able to make the upgrade, Vue 2.7 will be bringing a large number of features for Vue 3 back to Vue 2. Remember - this is a major upgrade. Errors and warnings are expected. It could be frustrating at some points. Keep this in mind, and stay curious! If you weren't following along, here's the link to the application we upgraded. Have a good one!...
Jun 30, 2021
Custom Composable Methods with Vue 3
May 20, 2021
Provide/Inject API With Vue 3
Introduction One of the most difficult problems to solve when building single-page applications is state management. With component-based frameworks like Vue, this is typically solved in one of two ways: 1. State is managed by components, with data being passed to child components as props. The parent component then listens for events and performs actions on the state accordingly. 2. State is managed with a global state management library (Vuex, Redux, etc). Global state is then injected into the desired components, and those components triggers actions in the state management library (such as API requests or data updates). This can provide a layer of separation between logic and templating which is useful, and helps with passing data between parts of the application. Vue offers an interesting middleground between these two approaches with the Provide/Inject API. This API is similar to React's Context API, in that it allows a component to provide some sort of data to any component beneath it in the component tree. For example, a parent component could provide a piece of data (say, the user's username), and then a grandchild component could inject that value into itself. Provide/Inject gives developers a way to share data between parent and child components while avoiding prop drilling (passing a prop from one component to the next in a chain). This can make your code more readable, and reduce the complexity of your props for any components that don't rely on this data. In this article, we will explore a basic example using the Provide/Inject API, building up a new application with a user dashboard to update their name and email. Getting Started - Using Props We'll first set up our example application using the standard approach to passing data between components - props. Below is our homepage and a navigation page. ` ` This is a very straightforward parent/child structure. Our parent component has a reactive object (state) with a user's name and email. The username is passed into the Nav component as a prop, and then displayed. This works well because we only have two components, but what if there were other layout components between the root and the navigation component? We can use Provide/Inject to send this data from the parent to the navigation component. In our parent component, we will provide the data we want available (the username), and then inject that data into the Nav component. Provide/Inject with Composition API Let's start with App.vue, and explore how to use provide. Below is our rewritten root component: ` With Vue 3, we have access to a Composition API method provide. This function takes a key and a value. The key is how the provided value will be accessed in other components. In this example. we are passing a computed property with the user's username as the value. Why are we passing a computed property? By default, the provided value is not reactive. If we just wrote provide('username', state.name), any component that injected this value would only have the initial state of the username. If it were to change in the future, the name would be out of sync with the root component. If we wanted to provide the entire state, we could write this instead: ` That's because we're using a reactive object for our state. Alternatively, if the username was a ref, we could also use that in the same way. Keep in mind that any value could be passed as an argument to provide, including functions. This will come up later in our example, but it's important to think about when you're wanting to use this API. Let's look our our navigation component now, using inject to get the value of state.name. ` Similar to what we did in App.vue, we used a Composition API method called inject to get the value. inject takes the key we used when providing the data, and then returns the value as a variable. Since we provided a computed property, inject returns a computed property as well. inject has two other arguments as well: 1. defaultValue: This is the value that should be returned in the event a provided value is not found with that key. 2. treatDefaultAsFactory: As I noted above, any value (including functions) can be provided as a value to inject. In the event that a function is what you are providing, you don't want it to be invoked by mistake. But what if you're providing an object? Unless it is returned from a function, you could end up with duplicate objects that have the same reference (this is also why data and props recommend returning objects from a function rather than setting them directly). When using a function as the default value, this argument tells inject whether the default is the function itself, or the value returned by the function. The two below examples return the same default, a string: ` In our component example, the username is being injected and returned from setup, making it available in the template. We can then use that variable as we normally would, but without having to worry about props. Nice! This could save us a lot of time and effort with prop drilling. Providing Reactivity In the last section, we discussed reactivity and how the argument in provide needs to be a reactive object if we want data to stay in sync. Let's build out our user dashboard so they can update their name and email. In our App.vue, we're going to add a single line to our setup method: ` This will provide the entire state object (a reactive object) to whatever component wants to inject it. Now, let's build out a dashboard to work with that data: ` In this component, we inject the entire userDetails provided value, and return it to the template. We can then use v-model to bind directly to the injected values. With this in place, it all works as expected! Any changes made to the username field would properly update in the navigation as well. However, there's a small catch to doing things this way. Per the Vue 3 documentation, "When using reactive provide / inject values, it is recommended to keep any mutations to reactive properties inside of the provider whenever possible." The reason for this is that allowing any child component to mutate a value could lead to confusion about where a particular mutation is happening. The more disciplined our codebase is about mutating state, the more stable and predictable it will be. Rather than directly using v-model on our reactive state, let's provide a couple functions that will do the updating for us. First, we'll update App.vue to handle the new providers: ` We have added two functions - updateUsername and updateEmail. These functions are nearly identical, just updating the value on our state object that they are associated to. We then provide these two functions using the provide method, so that they are available to children components. Remember above when we discussed that any value could be provided? This is why treatDefaultAsFactory is important. Here, we are providing two functions that don't return anything. If inject by default invoked the function and returns its value, we would get undefined is not a function errors in our child component. In this case, we're really wanting a function to be injected into our component, so defaulting treatDefaultAsFactory to false is excellent. Here's the updated code for MyProfile.vue: ` Rather than binding directly on userDetails, we created two computed properties, each with a getter and setter. We can then bind to the computed properties, since they will return the desired value (username or email) and trigger the update methods we injected. Now our reactivity is fully controlled by the root component, App.vue, rather than the child. Global State Management I mentioned above that Provide/Inject gives us a middle ground between global state and component state. With the introduction of the Composition API, however, there's no reason we can't use Provide/Inject as our global management. Let's take everything we've written so far and extract it to a separate file: ` In this file, we have two functions - initStore and useStore. initStore creates our reactive object, getters for both the username and email, and methods to perform updates, then provides each of those values. These three groups (state, computed, and methods) maps very nicely to how Vuex works (state, getters, and actions). The second method, useStore, simply returns an object with the injected values. This lets us use the store we've created from a single location, so if we change the key used in provide, we can also update it in the inject. This ensures we aren't duplicating our inject calls, and we only have one file to check if something goes wrong. Our App.vue file is now a lot simpler: ` Since we don't need the store values in our root component, we can safely call initStore to generate the store, and provide its values to our child components. Then, in MyProfile.vue, we can do the following: ` Because useStore injects the values for us, we have access to the username, password, and their update methods. This is one of the ways that the Composition API can help keep our code clean, and our logic bundled by feature rather than functionality. If this concept interests you, there's a library for Vue 2 and 3 called Pinia that takes this approach to the next level. Pinia provides you a typesafe, easy to maintain global store. Check it out! Conclusion Using Provide/Inject can help remove some of the complexity of passing data between parent and child components. Keep in mind that provided values do not have the same checks as props, such as required or type, so they are inherently less safe to use. There is no guarantee that the value you want to inject is present in the component tree, nor do you know for certain what shape that value is in. Also, up until recently, the Vue documentation included, "provide and inject are primarily provided for advanced plugin / component library use cases. It is NOT recommended to use them in generic application code." This has since been removed, and libraries like Pinia show the power of using this API in application code. I would still recommend being careful when choosing to implement a feature using Provide and Inject. That said, have fun and try it out! Here's a link to a Stackblitz example of the final form of the appliation we worked through above. Until next time!...
May 10, 2021
Vue 3 Composition API - watch and watchEffect
Apr 29, 2021
Computing Application State in Vue 3
Introduction We've all been there before- working on an application, and suddently we need to determine what state something is in. Maybe it's whether the form has been submitted already, or the class a certain element should have. You may be tempted to set that value to a variable, and move on. What's the harm, right? It turns out that there are a number of reasons this could be a problem. Your state could be out of date due to a change from a different function. You could cause an 'impossible state', or a state in the UI that was never intended by the developer. And at the very least, your code is more imperative, meaning that you as the developer are having to write and maintain more lines of code. It's almost like manually juggling all the values in your application - what happens if you drop one? Luckily, Vue provides a solution for this - the computed property. With the computed property (or Composition API method), we can perform calculations like we described above by declaring them and getting a readonly, reactive ref to use in our application. Vue is able to determine when a dependency in the computed property has changed, and recalculate its result. We can then use these calculated values as if they were another variable, and use them in our template and logic with ease. Setting up our example Let's start with a common example: You have been tasked with building a form that accepts a name, email, and comments. We want to track the number of characters a user has entered, and allow them to submit the form. Below is an example of this form: ` The above form provides the basic functionality that we need. But there are a few things missing: - We're just logging out messages when the API comes back. We should report the error or success state to the user. - We probably shouldn't allow users to submit the form without filling out the fields. - We also shouldn't allow users to submit the form while their submission is being processed. We don't want to receive duplicate form entries. With this in mind, let's rewrite our code. The template is the same, but we'll need to track the form's state - whether it has been submitted or had an error, and whether the user can click the submit button. Sounds simple enough, so let's add some booleans - submitting, hasError, hasSuccess. That should handle the state. We don't want the user to have to click a "Validate" button, of course - that would be frustrating. So let's use the watch Composition API method, and calculate whether to show the submit button. Below is our updated code: ` The way this is written works, but there are a number of issues: - In the onSubmitFormHandler, we are manually resetting each status to where it should be. - In addition, the fact that we are using multiple booleans to track our form's state means that we could end up in an impossible state, where both "submitting" and "hasError" are true. That could lead to unexpected, and unpredictable, user experiences. - The watcher does the job of recalculating whenever the formState is changed, but we're still having the manually track this value. - We're still manually handling the input event on the comments field to get the character count. Most importantly, it's going to be much harder to refactor this going forward, because it's harder to determine what is going on. Is there a relationship between "hasError" and "hasSuccess"? What is calling "validateHasInput"? Implementing Computed Property Let's upgrade our application using the computed Composition method now. First, we'll replace the boolean states with a single state, and use computed properties to determine what state we are in. We can also use a computed property to get the character count, and remove that extra event on the comment textarea. Finally, we'll move the validateHasInput into its own computed property. Here's the updated form: ` We have now refactored to use the computed property. What does that give us? - The boolean values (submitting, hasError, hasSuccess) are no longer being set imperatively. They are being calculated based off of a status variable. If we were using something like Typescript, we could force the type of status to be a certain subset, but for now, we are using an object with a few states - including IDLE. - The character count is now being calculated off of the length of the comment string, rather than checking the length from the emitted event. This means that if we change the value of formState.comment programmatically, our character count is up to date without us having to change anything. - In the onSubmitFormHandler, we set which status our form is in as we go. We aren't having to set each status individually, which means we don't have the possibility of impossible states. Using Getters in Vuex Our application is in a much better state now, but there is still room to improve. One way we can better encapsulate our logic is by utilizing Vuex for managing the state of the form submission. Luckily, we can implement our computed logic in Vuex pretty easily with getters. In Vuex, getters fill the same role as computed properties in single-file components. Let's move our API logic out of the component, and into Vuex: ` In Vuex, we create a store, which is then plugged into our app. This store contains our state (the submission status), a single mutation to handle updating the state, and an action for submitting the form. We then have our four computed properties, which match the three we had previously as well as getting the raw state. By using Vuex, the state of our form submission is disconnected from the template. This can be useful when building out larger applcations, so that your components remain focused on the user experience, and the logic is handled within your global state management. Here's what our updated component's logic looks like: ` In our component, we are now importing useStore in order to access our Vuex store. The component then makes an API call via a dispatch, which calls our action. Neat! Using Get/Set with Computed Properties One more nice feature of computed properties is how they interact with ES5 getters and setters. If you aren't aware, with ES5 you can add a function to an object that is either a get() or a set(val). This function is not invoked like a normal function, but instead, whenever the value is read or assigned to. For example: ` With computed properties, we can leverage this system to build additional functionality. Let's say that you want to add a reset function to the form. One potential way to do that could be like this: ` With the Composition API, if you pass an object in as the first parameter (rather than a function), you can use the get and set keys to make your own custom getter and setter. This way, rather than making the function call directly to store.commit, we can simply set the currentStatus to the status we want. This ability to set to computed properties is especially useful when using v-model on a custom component. Below is an example where this can help to model a custom input. ` By leveraging getters and setters in your computed properties, you can cut down on the amount of code you need to write in order to perform bindings between components and Vuex. You can also add additional logic, such as validation or data cleanup. Just make sure that you aren't overburdening your setters- if they start to get large, you might need to use a separate function anyway. Conclusion Computed properties can help ensure that your applications are easy to maintain and understandable. By leveraging a computed property, rather than manually assigning values, you allow the framework to do the heavy lifting for you. Computed properties are great for a number of complex tasks, such as: - Form status - API calls - Formulaic calculations (temperature conversion, weight conversion) - State-based CSS classes - Determining which state to render your component in. Keep in mind - computed properties should not cause side effects. If you are writing a computed property that needs to alter your state, it should probably be a watcher. Take a look at your own applications, and see where using a computed property could be beneficial. Have fun!...
Apr 27, 2021
Improve User Experience in Vue 3 with Suspense
Not every user will have the same connection speed that developers have. They may experience parts of your application that appear to be broken or stuck while content is loading. Vue 3 provides a new way to handle situations like this, called Suspense....
Mar 24, 2021