⭐ If you would like to buy me a coffee, well thank you very much that is mega kind! : https://www.buymeacoffee.com/honeyvig Hire a web Developer and Designer to upgrade and boost your online presence with cutting edge Technologies
Showing posts with label vue. Show all posts
Showing posts with label vue. Show all posts

Thursday, March 30, 2023

Moving From Vue 1 To Vue 2 To Vue 3: A Case Study Of Migrating A Headless CMS System

 While migrating large front-end systems is usually a daunting task, it can be helpful to understand the reasons and strategies behind it. In this article, we explores its potential benefits and drawbacks and shares what considerations and steps were involved in the process of migrating the front-end interface of Storyblok’s headless content management system.

One of the greatest challenges in software development is not in creating new functionality but in maintaining and upgrading existing systems. With growing dependencies and complexity, it can be a tedious task to continuously keep everything updated. This becomes even more challenging when upgrading a base technology that the whole system runs on.

In this article, we will discuss how Storyblok solved the challenges of migrating the front-end interface of our headless content management system from Vue 1 to Vue 2 to Vue 3 within six years of growing the startup.

While migrating larger front-end systems can be a daunting task, it can be helpful to understand the reasons and strategies behind it. We’ll delve into the considerations and steps involved in such a migration and explore the potential benefits and drawbacks. With a clear understanding of the process, we can approach the migration with more confidence and ensure a smooth transition for our users and stakeholders.

The Vue Ecosystem & Storyblok’s Early Days

The Vue.js framework’s first large pre-beta release happened in late 2016, and Storyblok began work on a full prototype built on top of Vue in late 2015. At the time, Vue was still a relatively new framework, and other more established options like React were available. Despite this, Storyblok decided to take a chance on Vue and built their own prototype on top of it. This turned out to be a good decision, as the prototype worked well, and up to today, Vue is kept as the underlying framework for the front-end interface.

Over the years, Storyblok has played a key role in the growth and development of Vue, participating in forums, conferences, and meetups, sponsoring certain projects, as well as contributing to the Vue ecosystem through open-source projects and other initiatives. As Storyblok grew together with the Vue community over the years, Vue started upgrading its framework, and Storyblok began growing out of its prototype to become a fully-fledged product. This is where our migration story starts.

Ground-up Migration vs. Soft Migration

There were two main points in time when Storyblok was facing large migration challenges. The first one was when the upgrade from Vue 1 to Vue 2 happened. This went hand in hand with the update from Storyblok Version 1 to Storyblok Version 2. The decision was to completely rebuild the system from scratch. This is what we’ll call Ground-up migration. The second large migration happened when going from Vue 2 to Vue 3, where the front-end interface did not change. The existing codebase was updated in the background without visual changes for the user, and this is what we’ll call Soft migration.

Ground-up migration allows for greater flexibility and control over the design and architecture of the new system, but it can be a time-consuming and resource-intensive process. For Storyblok, it allowed the development of an open-source Blok Ink design system as the core of the new system. This design system could then simultaneously be used by our customers to build their own extensions.

Soft migration, on the other hand, can be quicker and more cost-effective, but it’s strongly limited and influenced by the current design and architecture of the existing system. Upgrading large codebases and all their dependencies can take months to achieve, and the time for this can be hard to find in a growing business. When thinking about customers, soft migration tends to be easier because the user doesn’t have to relearn a whole new interface, and no large marketing and communication resources need to be allocated towards this kind of migration.

How were these migration decisions made from a business perspective? After Storyblok was launched as a product in 2017 with Vue 1, it was continuously improved and extended with new features. In 2019, the team received its first seed investment, which allowed them to hire more developers to work on Storyblok. In 2020, work began on Storyblok V2 with Vue 2, with around five developers starting on the project for the first time. Instead of updating the old codebase from Vue 1 to Vue 2, the team decided to start with a completely new codebase. This gave the new team two main benefits:

  1. The developers were fully involved in creating the architecture of the new system;
  2. They learned how the system worked by rebuilding it.

In the core, the decision to make a ground-up migration was correct because the flexibility of that migration allowed the team to build a better, newer, and more stable version of the prototype while also understanding how it works. The main drawbacks of the ground-up migration were the cost of time and resources and getting the buy-in from the customers to switch from the old to the new version.

As Storyblok continued to evolve and improve, the codebase needed to be upgraded from Vue 2 to Vue 3. Migrating to the new version involved updating large amounts of code and ensuring that everything continued to work as expected. In this migration, we had to invest a lot of resources in retesting the interface, as well as allocating time for the developers to learn and understand the changes in Vue 3. This can be especially challenging when working with a large team, as there may be different codebases and business needs to consider.

Ultimately, the decision between these two approaches will depend on the specific needs and circumstances of the migration, including the following:

  • Resources and expertise available,
  • Complexity and size of the system,
  • Desired level of control and flexibility over the design and architecture of the new system.

It’s important to have a clear plan in place to guide the migration process and ensure a smooth transition, so in the next chapter, we will look into what that plan looked like for us.

Strategies For Large Migrations

These five factors are essential to consider when planning a migration:

TimeCreating a timeline for the migration
FunctionalityIdentify and prioritize critical parts of the system to migrate
ResourcesUse automation and tools to support the migration
AcceptanceEngage and communicate with users
RiskMonitor and evaluate the migration process

Creating A Timeline

One of the main challenges of migration is getting the buy-in from the organization, clients, and teams. Since migrations aren’t adding any new functionality, it can be hard to convince a team and the product owners of the importance of migration. However,

In the long term, migrations are necessary, and the longer you put them off, the harder they will become.

Secondly, migrations tend to take more than a few weeks, so it’s a harder and more tedious process that has to be planned with all the developers and stakeholders. Here is what our timeline looked like:

Going from Vue 1 to Vue 2

This process took us around two years from start to finish:

  • Before Mid–2020: Develop new features in the old version.
  • 2020 June: Identify and develop ‘core’ components in a new open-source design system.
  • 2020 November: Create a new Vue 2 project.
  • 2020 Nov — 2021 August: Redevelop all parts of the old app in the new app with the new design system.
  • 2021 August: Beta Release of parts of the new application (e.g., our Visual Editor).
  • 2021 August — 2022 August: Add all the missing functionality from the old app, add new features, and improve overall UX through customer feedback.
  • 2022 August: Official release of the new app.
  • During that time: onboard 20+ developers, designers, QAs, and product owners.

Going from Vue 2 to Vue 3

This process took us around eight months:

  • 2022 July — 2022 November: Start migrating the Design System to Vue 3.
  • 2022 November — 2023 January: remove all the ‘old’ breaking functionality and update or replace old dependencies that depend on Vue 2.
  • 2022 December: Several meetings to explain what changed in Vue 3 with developers and stakeholders and how the transition will happen.
  • 2023 January: Introduce the Vue 3 codebase, switch developers to the new codebase, and thoroughly test and start developing in Vue 3.
  • 2023 February: Launch the Vue 3 version into production.

To create a timeline, you need to identify all the parts of the migration first. This can be done in an excel sheet or a planning tool like Jira. Here is an example of a simple sheet that could be used for creating a rough timeline. It can be useful to split different areas to separate sheets to divide them into the teams they belong to.

Identifying And Prioritizing Functionality to Migrate 

In order to manage the cost and resources required for the migration, it is essential to identify and prioritize the critical features and functionality of the system that need to be migrated. For us, it meant starting bit by bit with more important functionality. When we built the new version of Storyblok, we started with our most important core feature, the “Visual Editor,” and built an entirely new version of it using our Design System. In any migration, you should ask yourself these questions to find out what the priorities are:

  • Is the goal to create a completely new version of something?
  • What parts does the system have? Can some of them be migrated separately?
  • Which parts of the system are most important to the customer?
  • Who knows the system well and can help with the migration?
  • Does every developer need to work on the migration, or can a few ‘experts’ be selected to focus on this task?
  • Can I estimate how long the migration will take? If not, can I break down the parts of the system more to find out how long smaller parts of the system can be migrated?

If you have multiple sub-parts that can be migrated separately, it’s easier to split them to different people to work on the migration. Another big decision is who is working on the migration. For our Vue 1 to Vue 2 migration, all our developers worked on creating the new functionality, but on the Vue 2 to Vue 3, it was one expert (the person who is writing this article) who did most of the migration and then it was handed over to the teams to finish and retest the whole application to see if everything was still working as it should.

At the same time, I organized some training for the developers to dive deeper into Vue 3 and the breaking changes in the system.

The core of the migration strategy always depends on the knowledge of the system as well as the importance of the features to be migrated.

More important features will be migrated and worked on first, and less important things can be kept to be improved in the end.

Use Automation And Tools to Support The Migration

Since a migration process is always very time-consuming, it pays off to invest in some automation of tedious tasks to find out all the problems of the migration. For our migration from Vue 2 to Vue 3, the migration build and its linting were very helpful in finding all the potential problem areas in the app that needed to be changed. We also worked with hand-written scripts that iterate through all Vue files in the project (we have over 600 of them) to automatically add all missing emit notations, replace event names, that updated in Vue 3 and update common logic changes in the unit tests, like adding the global notation.

These scripts were a large timesaver since we didn’t have to touch hundreds of files by hand, so investing time in writing such scripts can really pay off. In the core, utilizing regex to find and replace large logic chunks can help, but for the last final stretch, you will still spend hours fixing some things manually.

In the final part, all the unit and end-to-end tests we already had, helped to find some of the potential problems in the new version of our app. For unit testing, we used Jest with the built-in Test Utils as well as the Vue Testing Library, for the e2e tests, we’re using Cypress with different plugins.

Engage And Communicate With Users

If you create a new version of something, it’s essential to make sure that the experience is not getting worse for the customer. This can involve providing training and support to help users understand and use the new version, as well as collecting feedback and suggestions from users to help improve the new system. For us, this was a very important learning during the ground-up migration from Vue 1 to Vue 2 because we continuously collected feedback from the customer while rebuilding the app at the same time. This helped us to ensure that what we were building was what the customers wanted.

Another way was to have the beta version accessible way ahead of the new version being finished. In the beginning, we only made the Partner Portal available, then the Visual Editor, then the Content Editing, and lastly, the missing parts of the app. By gradually introducing the new parts, we could collect feedback during the development time and adjust things where the customer experience was not perfect yet.

In any scenario, it will be important to ask yourself these questions in a ground-up migration:

  • How can we collect feedback from our customers early and often to ensure what we’re building is working?
  • How can we communicate with the existing customers to make them aware of using the new version?
  • What are the communication channels for the update? Can we leverage the update with the marketing team to make users more excited about it?
  • Who do we have internally that handles communication, and who are the stakeholders who should be updated regularly on the changes?

For us, this meant building ‘feedback buttons’ into the interface to collect feedback from the users starting to use the new version. It pointed to a form where the users could rate specific parts but also give open feedback. This feedback was then handed back to the design team to evaluate and forward if it was valid feedback. Further, we added channels in our Discord to hear directly from developers using the system and introduced ‘tour’ functionalities that showed users where all the buttons and features are located and what they do. Finally, we added buttons to the old app to seamlessly switch between the old and new versions. We did various campaigns and videos on social media to hype our community about using the new version.

In all cases, it’s crucial to find out who the core stakeholders and communicators of the migration are. You can find that out with a simple impact/influence matrix. This matrix documents who is impacted by the migration and who should have how much influence over the migration. This can indicate who you should be in close contact with and who might only need to be communicated with occasionally.


Monitor And Evaluate The Migration Process

Since a migration process will always take a few months or even years to accomplish, it’s essential to monitor the process and make sure that after a few months, it’s still going in the right direction.

Here are some questions that can help you figure out if you’re on the right track:

  • Have all the areas and functionalities that need to be migrated been defined? How are we tracking the process of each area?
  • Are all stakeholders, including users and other teams, being effectively communicated with and their concerns addressed?
  • Are there unexpected issues that have arisen during the migration that require you to adjust the budget and time frame of the migration?
  • Is the new version being properly tested and validated before being put into production?
  • Are the final results of the migration meeting the expectations of the users and stakeholders?

During the different migration phases, we hit several roadblocks. One of them was the customer’s expectations of having the exact same functionality from the old app in the new app. Initially, we wanted to change some of the UX and deprecate some features that weren’t satisfying to us. But over time, we noticed that it was really important to customers to be close to what they already knew from the older version, so over time, we moved many parts to be closer to how the customer expected it to be from before.

The second big roadblock in the migration from Vue 2 to Vue 3 was the migration of all our testing environments. Initially, we didn’t expect to have to put so much effort into updating the unit tests, but updating them was, at times, more time-consuming than the app itself. The testing in the “soft migration” had to be very extensive, and it took us more than three weeks to find and fix all the issues. During this time, we depended heavily on the skills of our QA engineers to help us figure out anything that might not work anymore.


The final step in the migration from Vue 2 to Vue 3 was to put the new version of the app into production. Since we had two versions of our app, one in Vue 2 and one in Vue 3, which looked essentially the same, we decided to do a blue/green deployment. This means that we transferred a subset of the user traffic to the new app while the majority of the users were still using the stable old app. We then tested this in production for a week with only a small subset of our users.

By slowly introducing the new version to just a percentage of the users, we could find more potential issues without disrupting all of our users. The essential part here was to have direct communication with our Sales and Support teams, who are in direct contact with important clients.

Lessons Learnt

Migration to Vue 2

The core lesson when we completely rebuilt Storyblok in Vue 2 was that migrating a large system can be a significant challenge for the development team that consists of new developers who are not familiar with the system yet. By handing the power over to the developers, they could be directly involved in forming the architecture, quality, and direction of the product. In any migration or significant change, the onboarding and training of developers will be an essential part of making the migration successful.

Another big lesson was the involvement of the existing users to improve the quality of the newer version we were building. By slowly introducing the new design and features in different areas of the system, the customers had the opportunity to get used to the new version gradually. With this gradual change, they could give feedback and report any issues or problems they encountered.

Overall, customers have a number of expectations when it comes to software, including:

  • Reliability and stability,
  • Ease of use,
  • Regular updates and improvements,
  • Support and assistance,
  • Security and privacy.

Migrating a large system can impact these expectations, and it’s important to carefully consider the potential impacts on customers and take steps to ensure a smooth transition for them.

Migration to Vue 3

As we got more teams and more customers, it kept getting more critical to keep our production version of the app as stable as possible, so testing was an important part of making sure the quality was monitored and bugs were eliminated. All that time we had invested in unit and e2e testing in the Vue 2 version helped us to find problems and bugs during the migration process to Vue 3.

We found that it was essential to test the migrated system extensively to ensure that it was functioning correctly and meeting all of the required specifications. By carefully planning and executing our testing process, we were able to identify and fix the majority of the issues before the migrated system was released to production.

During the Vue 3 migration, we also saw the advantages of having a ‘separate’ part of the system, our design system, that could be migrated first. This allowed us to learn and study the migration guide there first and then move to the more complex migration of the app itself.

Lastly, a big thing we learned was that communication is essential. We started creating internal communication channels on Slack to keep marketing and other teams up to date on all the functionality changes and new features. Certain people could then weigh in on the ways new features were built.

Conclusion

Migrating Storyblok from Vue 1 to Vue 2 to Vue 3 was a significant challenge for the whole organization. A well-formed migration plan should outline the steps to be taken, the areas and functionalities that need to be migrated and in what way (rebuild or create a new version), the people to be involved, and the expected timeline.

For us, some key takeaways were the importance of involving the development team in forming the architecture and direction of the product, as well as onboarding and training them properly. It was also essential to communicate effectively with stakeholders, including users and other teams, to address their concerns and ensure their expectations were met.

Testing plays a crucial role in ensuring the migrated system functions correctly, and the better your QA engineers, the more smoothly the migration will go. Gradually introducing the new version to a subset of users before a full release can help identify any potential issues without disrupting all users. Another approach is gradually introducing new parts of the system one by one. We made use of both of them.

If you’re planning a large system migration, starting with a well-defined plan is important. Make sure to consider budget and time frame, build in time for testing and validation, continuously monitor the progress of the migration and adjust the plan as necessary.

It’s also important to involve existing users in the migration process to gather feedback and identify any potential issues. Make sure your migration is actually improving the experience for the users or developers. Internal communication is key in this process, so ensure that all stakeholders are effectively communicated with throughout the migration to make sure everyone is on board. Consider gradually migrating different parts of the system to help manage complexity or introducing a new system only to a subset of users first.

And lastly, work with your team. Migrating really takes a lot of hands and time, so the better your teams can work together, the smoother the transition will go.

 

Friday, April 9, 2021

Reactivity In Vue

 Reactivity is the ability for a variable (array, string, number, object, etc) to update when its value or any other variable that it makes reference to is changed after declaration.

 

In this article, we’re going to look at reactivity in Vue, how it works, and how we can create reactive variables using newly created methods and functions. This article is targeted at developers who have a good understanding of how Vue 2.x works and are looking to get familiar with the new Vue 3.

We’re going to build a simple application to better understand this topic.

By default, JavaScript isn’t reactive. This means that if we create the variable boy and reference it in part A of our application, then proceed to modify boy in part B, part A will not update with the new value of boy.

let framework = 'Vue';
let sentence = `${framework} is awesome`;
console.log(sentence)
 // logs "Vue is awesome"
framework = 'React';
console.log(sentence)
//should log "React is awesome" if 'sentence' is reactive.

The snippet above is a perfect example of the non-reactive nature of JavaScript — hence, why the change isn’t reflected in the sentence variable.

In Vue 2.x, props, computed, and data() were all reactive by default, with the exception of properties that are not present in data when such components are created. This means that when a component is injected into the DOM, only the existing properties in the component’s data object would cause the component to update if and when such properties change.

Internally, Vue 3 uses the Proxy object (an ECMAScript 6 feature) to ensure that these properties are reactive, but it still provides the option to use Object.defineProperty from Vue 2 for Internet Explorer support (ECMAScript 5). This method defines a new property directly on an object, or modifies an existing property on an object, and returns the object.

At first glance and since most of us already know that reactivity is not new in Vue, it might seem unnecessary to make use of these properties, but the Options API has its limitations when you’re dealing with a large application with reusable functions in several parts of the application. To this end, the new Composition API was introduced to help with abstracting logic in order to make a code base easier to read and maintain. Also, we can now easily make any variable reactive regardless of its data type using any of the new properties and methods.

When we use the setup option, which serves as the entry point for the Composition API, the data object, computed properties, and methods are inaccessible because the component instance has not yet been created when setup is executed. This makes it impossible to take advantage of the built-in reactivity in any of these features in setup. In this tutorial, we’re going to learn about all of the ways we can do this.

The Reactive Method

According to the documentation, the reactive method, which is the equivalent of Vue.observable() in Vue 2.6, can be useful when we’re trying to create an object all of whose properties are reactive (such as the data object in the Options API). Under the hood, the data object in the Options API uses this method to make all of the properties in it reactive.

But we can create our own reactive object like this:

import { reactive } from 'vue'

// reactive state
let user = reactive({
        "id": 1,
        "name": "Leanne Graham",
        "username": "Bret",
        "email": "Sincere@april.biz",
        "address": {
            "street": "Kulas Light",
            "suite": "Apt. 556",
            "city": "Gwenborough",
            "zipcode": "92998-3874",
            "geo": {
                "lat": "-37.3159",
                "lng": "81.1496"
            }
        },
        "phone": "1-770-736-8031 x56442",
        "website": "hildegard.org",
        "company": {
            "name": "Romaguera-Crona",
            "catchPhrase": "Multi-layered client-server neural-net",
            "bs": "harness real-time e-markets"
        },
        "cars": {
            "number": 0
        }
    })

Here, we imported the reactive method from Vue, and then we declared our user variable by passing its value to this function as an argument. In doing so, we’ve made user reactive, and, thus, if we use user in our template and if either the object or a property of this object should change, then this value will get automatically updated in this template.

ref

Just as we have a method for making objects reactive, we also need one to make other standalone primitive values (strings, booleans, undefined values, numbers, etc.) and arrays reactive. During development, we would work with these other data types while also needing them to be reactive. The first approach we might think of would be to use reactive and pass in the value of the variable that we want to make reactive.

import { reactive } from 'vue'

const state = reactive({
  users: [],
});

Because reactive has deep reactive conversion, user as a property would also be reactive, thereby achieving our goal; hence, user would always update anywhere it is used in the template of such an app. But with the ref property, we can make any variable with any data type reactive by passing the value of that variable to ref. This method also works for objects, but it nests the object one level deeper than when the reactive method is used.

let property = {
  rooms: '4 rooms',
  garage: true,
  swimmingPool: false
}
let reactiveProperty = ref(property)
console.log(reactiveProperty)
// prints {
// value: {rooms: "4 rooms", garage: true, swimmingPool: false}
// }

Under the hood, ref takes this argument passed to it and converts it into an object with a key of value. This means, we can access our variable by calling variable.value, and we can also modify its value by calling it in the same way.

import {ref} from 'vue'
let age = ref(1)

console.log(age.value)
//prints 1
age.value++
console.log(age.value)
//prints 2

With this, we can import ref into our component and create a reactive variable:

<template>
  <div class="home">
    <form @click.prevent="">
      <table>
        <tr>
          <th>Name</th>
          <th>Username</th>
          <th>email</th>
          <th>Edit Cars</th>
          <th>Cars</th>
        </tr>
        <tr v-for="user in users" :key="user.id">
          <td>{{ user.name }}</td>
          <td>{{ user.username }}</td>
          <td>{{ user.email }}</td>
          <td>
            <input
              type="number"
              style="width: 20px;"
              name="cars"
              id="cars"
              v-model.number="user.cars.number"
            />
          </td>
          <td>
            <cars-number :cars="user.cars" />
          </td>
        </tr>
      </table>
      <p>Total number of cars: {{ getTotalCars }}</p>
    </form>
  </div>
</template>
<script>
  // @ is an alias to /src
  import carsNumber from "@/components/cars-number.vue";
  import axios from "axios";
  import { ref } from "vue";
  export default {
    name: "Home",
    data() {
      return {};
    },
    setup() {
      let users = ref([]);
      const getUsers = async () => {
        let { data } = await axios({
          url: "data.json",
        });
        users.value = data;
      };
      return {
        users,
        getUsers,
      };
    },
    components: {
      carsNumber,
    },
    created() {
      this.getUsers();
    },
    computed: {
      getTotalCars() {
        let users = this.users;
        let totalCars = users.reduce(function(sum, elem) {
          return sum + elem.cars.number;
        }, 0);
        return totalCars;
    },
  };
</script>

Here, we imported ref in order to create a reactive users variable in our component. We then imported axios to fetch data from a JSON file in the public folder, and we imported our carsNumber component, which we’ll be creating later on. The next thing we did was create a reactive users variable using the ref method, so that users can update whenever the response from our JSON file changes.

We also created a getUser function that fetches the users array from our JSON file using axios, and we assigned the value from this request to the users variable. Finally, we created a computed property that computes the total number of cars that our users have as we have modified it in the template section.

It is important to note that when accessing ref properties that are returned in the template section or outside of setup(), they are automatically shallow unwrapped. This means that refs that are an object would still require a .value in order to be accessed. Because users is an array, we could simply use users and not users.value in getTotalCars.

In the template section, we displayed a table that displays each user’s information, together with a <cars-number /> component. This component accepts a cars prop that is displayed in each user’s row as the number of cars they have. This value updates whenever the value of cars changes in the user object, which is exactly how the data object or computed property would work if we were working with the Options API.

 

toRefs

When we use the Composition API, the setup function accepts two arguments: props and context. This props is passed from the component to setup(), and it makes it possible to access the props that the component has from inside this new API. This method is particularly useful because it allows for the destructuring of objects without losing its reactivity.

<template>
  <p>{{ cars.number }}</p>
</template>
<script>
  export default {
    props: {
      cars: {
        type: Object,
        required: true,
      },
      gender: {
        type: String,
        required: true,
      },
    },
    setup(props) {
      console.log(props);
   // prints {gender: "female", cars: Proxy}
    },
  };
</script>
<style></style>

To use a value that is an object from props in the Composition API while ensuring it maintains its reactivity, we make use of toRefs. This method takes a reactive object and converts it into a plain object in which each property of the original reactive object becomes a ref. What this means is that the cars prop…

cars: {
  number: 0
}

… would now become this:

{
  value: cars: {
    number: 0
  }

With this, we can make use of cars inside any part of the setup API while still maintaining its reactivity.

 setup(props) {
      let { cars } = toRefs(props);
      console.log(cars.value);
      // prints {number: 0}
    },

We can watch this new variable using the Composition API’s watch and react to this change however we might want to.

setup(props) {
      let { cars } = toRefs(props);
      watch(
        () => cars,
        (cars, prevCars) => {
          console.log("deep ", cars.value, prevCars.value);
        },
        { deep: true }
      );
    }

toRef

Another common use case we could be faced with is passing a value that is not necessarily an object but rather one of the data types that work with ref (array, number, string, boolean, etc.). With toRef, we can create a reactive property (i.e. ref) from a source reactive object. Doing this would ensure that the property remains reactive and would update whenever the parent source changes.

const cars = reactive({
  Toyota: 1,
  Honda: 0
})

const NumberOfHondas = toRef(state, 'Honda')

NumberOfHondas.value++
console.log(state.Honda) // 1

state.Honda++
console.log(NumberOfHondas.value) // 2

Here, we created a reactive object using the reactive method, with the properties Toyota and Honda. We also made use of toRef to create a reactive variable out of Honda. From the example above, we can see that when we update Honda using either the reactive cars object or NumberOfHondas, the value gets updated in both instances.

This method is similar and yet so different from the toRefs method that we covered above in the sense that it maintains its connection to its source and can be used for strings, arrays, and numbers. Unlike with toRefs, we do not need to worry about the existence of the property in its source at the time of creation, because if this property does not exist at the time that this ref is created and instead returns null, it would still be stored as a valid property, with a form of watcher put in place, so that when this value changes, this ref created using toRef would also be updated.

We can also use this method to create a reactive property from props. That would look like this:

<template>
  <p>{{ cars.number }}</p>
</template>
<script>
  import { watch, toRefs, toRef } from "vue";
  export default {
    props: {
      cars: {
        type: Object,
        required: true,
      },
      gender: {
        type: String,
        required: true,
      },
    },
    setup(props) {
      let { cars } = toRefs(props);
      let gender = toRef(props, "gender");
      console.log(gender.value);
      watch(
        () => cars,
        (cars, prevCars) => {
          console.log("deep ", cars.value, prevCars.value);
        },
        { deep: true }
      );
    },
  };
</script>

Here, we created a ref that would be based on the gender property gotten from props. This comes in handy when we want to perform extra operations on the prop of a particular component.

Conclusion

In this article, we have looked at how reactivity in Vue works using some of the newly introduced methods and functions from Vue 3. We started by looking at what reactivity is and how Vue makes use of the Proxy object behind the scenes to achieve this. We also looked at how we can create reactive objects using reactive and how to create reactive properties using ref.

Finally, we looked at how to convert reactive objects to plain objects, each of whose properties are a ref pointing to the corresponding property of the original object, and we saw how to create a ref for a property on a reactive source object.

Further Resources

 

 

Thursday, October 24, 2019

Getting Started With Axios In Nuxt

In this tutorial, we will learn how to make a request in our Nuxt.js applications using the Axios module. We will also learn how to use the ayncData and fetch methods to fetch data on the server-side using Axios and the differences between the two methods. Finally, we will learn how to add authentication to our application using the Auth module.
Nuxt.js provides an Axios module for easy integration with your application. Axios is a promise-based HTTP client that works in the browser and Node.js environment or, in simpler terms, it is a tool for making requests (e.g API calls) in client-side applications and Node.js environment.
In this tutorial, we’re going to learn how to use the Axios module and how to make a request on the server-side using asyncData and fetch. These two methods make a request on the server-side but they have some differences which we’re also going to cover. Finally, we’ll learn how to perform authentication and secure pages/routes using the auth module and auth middleware.
This article requires basic knowledge of Nuxtjs and Vuejs as we’ll be building on top of that. For those without experience with Vuejs, I recommend you start from their official documentation and the Nuxt official page before continuing with this article.

What Is The Nuxt.js Axios Module?

According to the official Documentation,
“It is a Secure and easy Axios integration with Nuxt.js.”
Here are some of its features:
  1. Automatically set base URL for client-side & server-side.
  2. Proxy request headers in SSR (Useful for auth).
  3. Fetch Style requests.
  4. Integrated with Nuxt.js Progressbar while making requests.
To use the axios module in your application, you will have to first install it by using either npm or yarn.
YARN
yarn add @nuxtjs/axios
NPM
npm install @nuxtjs/axios
Add it into your nuxt.config.js file:
modules: [
    '@nuxtjs/axios',
  ],

  axios: {
    // extra config e.g
    // BaseURL: 'https://link-to-API'
  }
The modules array accepts a list of Nuxt.js modules such as dotenv, auth and in this case, Axios. What we’ve done is to inform our application that we would be using the Axios module, which we reference using @nuxtjs/axios. This is then followed by the axios property which is an object of configurations like the baseURL for both client-side and server-side.
Now, you can access Axios from anywhere in your application by calling this.$axios.method or this.$axios.$method. Where method can be get, post, or delete.


Making Your First Request Using Axios


After cloning the repo and opening the start folder, we would need to install all our packages in the package.json file so open your terminal and run the following command:
npm install
Once that is done, we can start our app using the npm run dev command. This is what you should see when you go to localhost:3000.

The next thing we have to do is to create a .env file in the root folder of our application and add our API URL to it. For this tutorial, we’ll be using a sample API built to collect reports from users.
API_URL=https://ireporter-endpoint.herokuapp.com/api/v2/
This way, we do not have to hard code our API into our app which is useful for working with two APIs (development and production).
The next step would be to open our nuxt.config.js file and add the environmental variable to our axios config that we added above.
/*
   ** Axios module configuration
   */
  axios: {
    // See https://github.com/nuxt-community/axios-module#options
    baseURL: process.env.API_URL,
  },
Here, we tell Nuxt.js to use this baseURL for both our client-side and server-side requests whenever we use this Axios module.
Now, to fetch a list of reports, let us open the index.vue file and add the following method to the script section.
async getIncidents() {
  let res = await this.$store.dispatch("getIncidents");
  this.incidents = res.data.data.incidents;
}
What we have done is to create an async function that we call getIncidents() and we can tell what it does from the name — it fetches a list of incidents using the Vuex store action method this.$store.dispatch. We assign the response from this action to our incidents property so we can be able to make use of it in the component.
We want to call the getIncidents() method whenever the component mounts. We can do that using the mounted hook.
mounted() {
    this.getIncidents()
  }
mounted() is a lifecycle hook that gets called when the component mounts. That will cause the call to the API to happen when the component mounts. Now, let us go into our index.js file in our store and create this action where we’ll be making our Axios request from.
export const actions = {
  async getIncidents() {
    let res = await this.$axios.get('/incidents')
    return res;
  }
}
Here, we created the action called getIncidents which is an async function, then we await a response from the server and return this response. The response from this action is sent back to our getIncidents() method in our index.vue file.
If we refresh our application, we should now be able to see a long list of incidents rendered on the page.

We have made our first request using Axios but we won’t stop there, we are going to be trying out asyncData and fetch to see the differences between them and using Axios.

AsyncData

AsyncData fetches data on the server-side and it’s called before loading the page component. It does not have access to this because it is called before your page component data is created. this is only available after the created hook has been called so Nuxt.js automatically merges the returned data into the component’s data.
Using asyncData is good for SEO because it fetches your site’s content on the server-side and also helps in loading content faster. Note that asyncData method can only be used in the pages folder of your application as it would not work in the components folder. This is because asyncData hook gets called before your component is created.

Let us add asyncData to our index.vue file and observe how fast our incidents data loads. Add the following code after our components property and let us get rid of our mounted hook.
async asyncData({ $axios }) {
    let { data } = await $axios.get("/incidents");
    return { incidents: data.data.incidents };
  },
  // mounted() {
  //   this.getIncidents();
  // },
Here, the asyncData method accepts a property from the context $axios. We use this property to fetch the list of incidents and the value is then returned. This value is automatically injected into our component. Now, you can notice how fast your content loads if you refresh the page and at no time is there no incident to render.

Fetch

The Fetch method is also used to make requests on the server-side. It is called after the created hook in the life cycle which means it has access to the component’s data. Unlike the asyncData method, the fetch method can be used in all .vue files and be used with the Vuex store. This means that if you have the following in your data function.
data() {
    return {
      incidents: [],
      id: 5,
      gender: 'male'
    };
}
You can easily modify id or gender by calling this.id or this.gender.

Using Axios As A Plugin

During the process of development with Axios, you might find that you need extra configuration like creating instances and interceptors for your request so your application can work as intended and thankfully, we can do that by extending our Axios into a plugin.
To extend axios, you have to create a plugin (e.g. axios.js) in your plugins folder.
export default function ({
  $axios,
  store,
  redirect
}) {
  $axios.onError(error => {
    if (error.response && error.response.status === 500) {
      redirect('/login')
    }
  })
  $axios.interceptors.response.use(
    response => {
      if (response.status === 200) {
        if (response.request.responseURL && response.request.responseURL.includes('login')) {
          store.dispatch("setUser", response);
        }
      }
      return response
    }
  )
}
This is an example of a plugin I wrote for a Nuxt application. Here, your function takes in a context object of $axios, store and redirect which we would use in configuring the plugin. The first thing we do is to listen for an error with a status of 500 using $axios.onError and redirect the user to the login page.
We also have an interceptor that intercepts every request response we make in our application checks if the status of the response we get is 200. If that is true we proceed and check that there is a response.request.responseURL and if it includes login. If this checks out to be true, we then send this response using our store’s dispatch method where it then mutated in our state.
Add this plugin to your nuxt.config.js file:
plugins: [
    '~/plugins/axios'
  ]
After doing this, your Axios plugin would intercept any request you make and check if you have defined a special case for it.

Introduction To The Auth Module

The auth module is used for performing authentication for your Nuxt application and can be accessed from anywhere in your application using $this.auth. It is also available in fetch, asyncData, middleware and NuxtInitServer from the context object as $auth.
The context provides additional objects/params from Nuxt to Vue components and is available in special nuxt lifecycle areas like those mentioned above.
To use the auth module in your application, you would have to install it using yarn or npm.
YARN
yarn add @nuxtjs/auth
NPM
npm install @nuxtjs/auth
Add it to your nuxt.config.js file.
modules: [
  '@nuxtjs/auth'
],
auth: {
  // Options
}
The auth property accepts a list of properties such as strategies and redirect. Here, strategies accepts your preferred authentication method which can be:
  • local
    For username/email and password-based flow.
  • facebook
    For using Facebook accounts as a means of authentication.
  • Github
    For authenticating users with Github accounts.
  • Google
    For authenticating users with Google accounts.
  • Auth0
  • Laravel Passport
The redirect property accepts an object of links for:
  • login
    Users would be redirected to this link if login is required.
  • logout
    Users would be redirected here if after logout current route is protected.
  • home
    Users would be redirected here after login.
Now, let us add the following to our nuxt.config.js file.
/*
 ** Auth module configuration
 */
auth: {
  redirect: {
    login: '/login',
    logout: '/',
    home: '/my-reports'
  },
  strategies: {
    local: {
      endpoints: {
        login: {
          url: "/user/login",
          method: "post",
          propertyName: "data.token",
        },
        logout: false,
        user: false,
      },
      tokenType: '',
      tokenName: 'x-auth',
      autoFetchUser: false
    },
  },
}
Please note that the auth method works best when there is a user endpoint provided in the option above.
Inside the auth config object, we have a redirect option in which we set our login route to /login, logout route to / and home route to /my-reports which would all behave as expected. We also have a tokenType property which represents the Authorization type in the header of our Axios request. It is set to Bearer by default and can be changed to work with your API.
For our API, there is no token type and this is why we’re going to leave it as an empty string. The tokenName represents the Authorization name (or the header property you want to attach your token to) inside your header in your Axios request.
By default, it is set to Authorization but for our API, the Authorization name is x-auth. The autoFetchUser property is used to enable user fetch object using the user endpoint property after login. It is true by default but our API does not have a user endpoint so we have set that to false.
For this tutorial, we would be using the local strategy. In our strategies, we have the local option with endpoints for login, user and logout but in our case, we would only use the *login* option because our demo API does not have a *logout* endpoint and our user object is being returned when *login* is successful.
Note: The auth module does not have a register endpoint option so that means we’re going to register the traditional way and redirect the user to the login page where we will perform the authentication using this.$auth.loginWith. This is the method used in authenticating your users. It accepts a ‘strategy’ (e.g local) as a first argument and then an object to perform this authentication with. Take a look at the following example.
let data {
          email: 'test@test.com',
          password: '123456'
}
this.$auth.loginWith('local', { data })

Using The Auth Module

Now that we have configured our auth module, we can proceed to our registration page. If you visit the /register page, you should see a registration form.

Let us make this form functional by adding the following code:
methods: {
  async registerUser() {
    this.loading = true;
    let data = this.register;
    try {
      await this.$axios.post("/user/create", data);
      this.$router.push("/login");
      this.loading = false;
      this.$notify({
        group: "success",
        title: "Success!",
        text: "Account created successfully"
      });
    } catch (error) {
      this.loading = false;
      this.$notify({
        group: "error",
        title: "Error!",
        text: error.response
          ? error.response.data.error
          : "Sorry an error occured, check your internet"
      });
    }
  }
}
Here, we have an async function called registerUser which is tied to a click event in our template and makes an Axios request wrapped in a try/catch block to an endpoint /user/create. This redirects to the /login page and notifies the user of a successful registration. We also have a catch block that alerts the user of any error if the request is not successful.
If the registration is successful, you would be redirected to the login page.

Here, we’re going to make use of auth authentication method this.$auth.loginWith('local', loginData) after which we would use the this.$auth.setUser(userObj) to set the user in our auth instance.
To get the login page working, let’s add the following code to our login.vue file.
methods: {
  async logIn() {
    let data = this.login;
    this.loading = true;
    try {
      let res = await this.$auth.loginWith("local", {
        data
      });
      this.loading = false;
      let user = res.data.data.user;
      this.$auth.setUser(user);
      this.$notify({
        group: "success",
        title: "Success!",
        text: "Welcome!"
      });
    } catch (error) {
      this.loading = false;
      this.$notify({
        group: "error",
        title: "Error!",
        text: error.response
          ? error.response.data.error
          : "Sorry an error occured, check your internet"
      });
    }
  }
}
We created an async function called logIn using the auth method this.$auth.loginWith('local, loginData). If this login attempt is successful, we then assign the user data to our auth instance using this.$auth.setUser(userInfo) and redirect the user to the /my-report page.
You can now get user data using this.$auth.user or with Vuex using this.$store.state.auth.user but that’s not all. The auth instance contains some other properties which you can see if you log in or check your state using your Vue dev tools.
If you log this.$store.state.auth to the console, you’ll see this:
{
  "auth": {
    "user": {
      "id": "d7a5efdf-0c29-48aa-9255-be818301d602",
      "email": "tmxo@test.com",
      "lastName": "Xo",
      "firstName": "Tm",
      "othernames": null,
      "isAdmin": false,
      "phoneNumber": null,
      "username": null
    },
    "loggedIn": true,
    "strategy": "local",
    "busy": false
  }
}
The auth instance contains a loggedIn property that is useful in switching between authenticated links in the nav/header section of your application. It also contains a strategy method that states the type of strategy the instance is running (e.g local).
Now, we will make use of this loggedIn property to arrange our nav links. Update your navBar component to the following:
<template>
  <header class="header">
    <div class="logo">
      <nuxt-link to="/">
        <Logo />
      </nuxt-link>
    </div>
    <nav class="nav">
      <div class="nav__user" v-if="auth.loggedIn">
        <p>{{ auth.user.email }}</p>
        <button class="nav__link nav__link--long">
          <nuxt-link to="/report-incident">Report incident</nuxt-link>
        </button>
        <button class="nav__link nav__link--long">
          <nuxt-link to="/my-reports">My Reports</nuxt-link>
        </button>
        <button class="nav__link" @click.prevent="logOut">Log out</button>
      </div>
      <button class="nav__link" v-if="!auth.loggedIn">
        <nuxt-link to="/login">Login</nuxt-link>
      </button>
      <button class="nav__link" v-if="!auth.loggedIn">
        <nuxt-link to="/register">Register</nuxt-link>
      </button>
    </nav>
  </header>
</template>
<script>
import { mapState } from "vuex";
import Logo from "@/components/Logo";
export default {
  name: "nav-bar",
  data() {
    return {};
  },
  computed: {
    ...mapState(["auth"])
  },
  methods: {
    logOut() {
      this.$store.dispatch("logOut");
      this.$router.push("/login");
    }
  },
  components: {
    Logo
  }
};
</script>
<style></style>
In our template section, we have several links to different parts of the application in which we are now using auth.loggedIn to display the appropriate links depending on the authentication status. We have a logout button that has a click event with a logOut() function attached to it. We also display the user’s email gotten from the auth property which is accessed from our Vuex store using the mapState method which maps our state auth to the computed property of the nav component. We also have a logout method that calls our Vuex action logOut and redirects the user to the login page.
Now, let us go ahead and update our store to have a logOut action.
export const actions = {
    // ....
  logOut() {
    this.$auth.logout();
  }
}
The logOut action calls the auth logout method which clears user data, deletes tokens from localStorage and sets loggedIn to false.
Routes like /my-reports and report-incident should not be visible to guests but at this point in our app, that is not the case. Nuxt does not have a navigation guard that can protect your routes, but it has is the auth middleware. It gives you the freedom to create your own middleware so you can configure it to work the way you want.
It can be set in two ways:
  1. Per route.
  2. Globally for the whole app in your nuxt.config.js file.
router: {
  middleware: ['auth']
}
This auth middleware works with your auth instance so you do not need to create an auth.js file in your middleware folder.
Let us now add this middleware to our my-reports.vue and report-incident.vue files. Add the following lines of code to the script section of each file.
middleware: 'auth'
Now, our application would check if the user trying to access these routes has an auth.loggedIn value of true. It’ll redirect them to the login page using our redirect option in our auth config file — if you’re not logged in and you try to visit either /my-report or report-incident, you would be redirected to /login.


This page is for adding incidents but that right now the form does not send incident to our server because we are not making the call to the server when the user attempts to submit the form. To solve this, we will add a reportIncident method which will be called when the user clicks on Report. We’ll have this in the script section of the component. This method will send the form data to the server. Update your report-incident.vue file with the following:
<template>
  <section class="report">
    <h1 class="report__heading">Report an Incident</h1>
    <form class="report__form">
      <div class="input__container">
        <label for="title" class="input__label">Title</label>
        <input
          type="text"
          name="title"
          id="title"
          v-model="incident.title"
          class="input__field"
          required
        />
      </div>
      <div class="input__container">
        <label for="location" class="input__label">Location</label>
        <input
          type="text"
          name="location"
          id="location"
          v-model="incident.location"
          required
          class="input__field"
        />
      </div>
      <div class="input__container">
        <label for="comment" class="input__label">Comment</label>
        <textarea
          name="comment"
          id="comment"
          v-model="incident.comment"
          class="input__area"
          cols="30"
          rows="10"
          required
        ></textarea>
      </div>
      <input type="submit" value="Report" class="input__button" @click.prevent="reportIncident" />
      <p class="loading__indicator" v-if="loading">Please wait....</p>
    </form>
  </section>
</template>
<script>
export default {
  name: "report-incident",
  middleware: "auth",
  data() {
    return {
      loading: false,
      incident: {
        type: "red-flag",
        title: "",
        location: "",
        comment: ""
      }
    };
  },
  methods: {
    async reportIncident() {
      let data = this.incident;
      let formData = new FormData();
      formData.append("title", data.title);
      formData.append("type", data.type);
      formData.append("location", data.location);
      formData.append("comment", data.comment);
      this.loading = true;
      try {
        let res = await this.$store.dispatch("reportIncident", formData);
        this.$notify({
          group: "success",
          title: "Success",
          text: "Incident reported successfully!"
        });
        this.loading = false;
        this.$router.push("/my-reports");
      } catch (error) {
        this.loading = false;
        this.$notify({
          group: "error",
          title: "Error!",
          text: error.response
            ? error.response.data.error
            : "Sorry an error occured, check your internet"
        });
      }
    }
  }
};
</script>
<style>
</style>
Here, we have a form with input fields for title, location, and comment with two-way data binding using v-model. We also have a submit button with a click event. In the script section, we have a reportIncident method that collects all the information provided in the form and is sent to our server using FormData because the API is designed to also accept images and videos.
This formData is attached to a Vuex action using the dispatch method, if the request is successful, you get redirected to /my-reports with a notification informing you that this request was successful otherwise, you would be notified of an error with the error message.
At this point, we don’t have reportIncident action in our store yet so in your browser console, you would see an error if you try to click submit on this page.

To fix this, add the reportIncident action your index.js file.
   
export const actions = {
  // ...
  async reportIncident({}, data) {
    let res = await this.$axios.post('/incident/create', data)
    return res;
  }
}
Here, we have a reportIncident function that takes in an empty context object and the data we’re sending from our form. This data is then attached to a post request that creates an incident and returns back to our report-incident.vue file.
At this point, you should be able to add a report using the form after which you would be redirected to /my-reports page.

This page should display a list of incidents created by the user but right now it only shows what we see above, let’s go ahead to fix that.
We’re going to be using the fetch method we learned about to get this list. Update your my-reports.vue file with the following:
<script>
import incidentCard from "@/components/incidentCard.vue";
export default {
  middleware: "auth",
  name: "my-reports",
  data() {
    return {
      incidents: []
    };
  },
  components: {
    incidentCard
  },
  async fetch() {
    let { data } = await this.$axios.get("/user/incidents");
    this.incidents = data.data;
  }
};
</script>
Here, we use fetch method to get user-specific incidents and assign the response to our incidents array.

At this point, we would notice a difference in how fetch method and asyncData loads our data.

Conclusion

So far, we have learned about the Axios module and all of its features. We have also learned more about asyncData, and how we can fetch both of them together despite their differences. We’ve also learned how to perform authentication in our application using the auth module and how to use the auth middleware to protect our routes. Here are some useful resources that talk more about all we’ve covered.

Resources

  1. Auth Module,” NuxtJS.org
  2. Axios Module: Introduction,” NuxtJS.org
  3. FormData, MDN web docs
  4. API: The asyncData Method,” NuxtJS.org
  5. The Vue Instance: Lifecycle Diagram,” VueJS.org
  6. Understanding How fetch Works In Nuxt 2.12,” NuxtJS.org