Skip to content

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:

<template>
  <form @submit.prevent="onSubmitFormHandler">
    <label for="name">
      Name
      <input id="name" type="text" v-model="formState.name" />
    </label>
    <label for="email">
      Email
      <input id="email" type="email" v-model="formState.email" />
    </label>
    <label for="comment">
      Comments
      <textarea
        id="comment"
        v-model="formState.comment"
        @input="updateCharacterCount"
      />
      {{ characterCount }} character(s)
    </label>
    <button>Submit Feedback</button>
  </form>
</template>

<script>
import { reactive, defineComponent, ref } from "vue";
import axios from "axios";

export default defineComponent({
  setup() {
    const formState = reactive({
      name: "",
      email: "",
      comment: "",
    });

    const characterCount = ref(0);

    const updateCharacterCount = (e) => {
      characterCount.value = e.target.value.length
    };

    const onSubmitFormHandler = () => {
      axios
        .post("http://localhost:3000/api/comment", formState)
        .then((res) => console.log(res))
        .catch((err) => console.log(err));
    };

    return {
      formState,
      characterCount,
      updateCharacterCount,
      onSubmitFormHandler,
    };
  },
});
</script>

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:

<template>
  <form @submit.prevent="onSubmitFormHandler">
    <label for="name">
      Name
      <input id="name" type="text" v-model="formState.name" />
    </label>
    <label for="email">
      Email
      <input id="email" type="email" v-model="formState.email" />
    </label>
    <label for="comment">
      Comments
      <textarea id="comment" v-model="formState.comment" @input="updateCharacterCount" />
      {{ characterCount }} character(s)
    </label>
    <button :disabled="submitting || !formReadyToSubmit">
      Submit Feedback
    </button>
  </form>
  <div>
    <template v-if="hasError">Something went wrong!</template>
    <template v-if="hasSuccess">Submitting successfully!</template>
  </div>
</template>

<script>
import { reactive, ref, watch, defineComponent } from "vue";
import axios from "axios";

export default defineComponent({
  setup() {
    const formState = reactive({
      name: "",
      email: "",
      comment: "",
    });

    const characterCount = ref(0);

    const submitting = ref(false);
    const hasError = ref(false);
    const hasSuccess = ref(false);

    const formReadyToSubmit = ref(false);

    const updateCharacterCount = (e) => {
      characterCount.value = e.target.value.length
    };

    const validateHasInput = () => {
      formReadyToSubmit.value =
        formState.name.length > 0 &&
        formState.email.length > 0 &&
        formState.comment.length > 0;
    };

    watch(
      () => ({ ...formState }),
      () => {
        validateHasInput();
      },
      {
        deep: true,
      }
    );

    const onSubmitFormHandler = () => {
      hasError.value = false;
      hasSuccess.value = false;
      submitting.value = true;

      axios
        .post("http://localhost:3000/api/comment", formState)
        .then((res) => {
          console.log(res);

          hasSuccess.value = true;
          submitting.value = false;
        })
        .catch((err) => {
          console.log(err);

          hasError.value = true;
          submitting.value = false;
        });
    };

    return {
      formState,
      onSubmitFormHandler,
      submitting,
      hasError,
      hasSuccess,
      formReadyToSubmit,
      characterCount,
      updateCharacterCount
    };
  },
});
</script>

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:

<template>
  <form @submit.prevent="onSubmitFormHandler">
    <label for="name">
      Name
      <input id="name" type="text" v-model="formState.name" />
    </label>
    <label for="email">
      Email
      <input id="email" type="email" v-model="formState.email" />
    </label>
    <label for="comment">
      Comments
      <textarea id="comment" v-model="formState.comment" />
      {{ characterCount }} character(s)
    </label>
    <button :disabled="!formReadyToSubmit">Submit Feedback</button>
  </form>
  <div>
    <template v-if="hasError">Something went wrong!</template>
    <template v-if="hasSuccess">Submitting successfully!</template>
  </div>
</template>

<script>
import { reactive, ref, computed, defineComponent } from "vue";
import axios from "axios";

const Status = {
  IDLE: "IDLE",
  SUBMITTING: "SUBMITTING",
  SUCCESS: "SUCCESS",
  ERROR: "ERROR",
};

export default defineComponent({
  setup() {
    const formState = reactive({
      name: "",
      email: "",
      comment: "",
    });
    const characterCount = computed(() => formState.comment.length);

    const status = ref(Status.IDLE);

    const submitting = computed(() => status === Status.SUBMITTING);
    const hasError = computed(() => status === Status.ERROR);
    const hasSuccess = computed(() => status === Status.SUCCESS);

    const formReadyToSubmit = computed(
      () =>
        !submitting.value &&
        formState.name.length > 0 &&
        formState.email.length > 0 &&
        formState.comment.length > 0
    );

    const onSubmitFormHandler = () => {
      status.value = Status.SUBMITTING;

      axios
        .post("http://localhost:3000/api/comment", formState)
        .then((res) => {
          console.log(res);

          status.value = Status.SUCCESS;
        })
        .catch((err) => {
          console.log(err);

          status.value = Status.ERROR;
        });
    };

    return {
      formState,
      onSubmitFormHandler,
      submitting,
      hasError,
      hasSuccess,
      formReadyToSubmit,
      characterCount
    };
  },
});
</script>

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:

import { createStore } from "vuex";
import axios from 'axios';

export const Status = {
  IDLE: "IDLE",
  SUBMITTING: "SUBMITTING",
  SUCCESS: "SUCCESS",
  ERROR: "ERROR",
};

export default createStore({
  state: {
    status: Status.IDLE
  },
  getters: {
    getStatus: state => state.status,
    submitting: state => state.status === Status.SUBMITTING,
    hasError: state => state.status === Status.ERROR,
    hasSuccess: state => state.status === Status.SUCCESS
  },
  mutations: {
    'SET_STATUS': (state, status) => state.status = status
  },
  actions: {
    submitForm: ({ commit }, payload) => {
      commit('SET_STATUS', Status.SUBMITTING);

      axios
        .post("http://localhost:3000/api/comment", payload)
        .then((res) => {
          console.log(res);

          commit('SET_STATUS', Status.SUCCESS);
        })
        .catch((err) => {
          console.log(err);

          commit('SET_STATUS', Status.ERROR);
        });
    }
  }
});

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:

import { reactive, computed, defineComponent } from "vue";
import { useStore } from "vuex";

export default defineComponent({
  setup() {
    const store = useStore();
    const formState = reactive({
      name: "",
      email: "",
      comment: "",
    });
    const characterCount = computed(() => formState.comment.length);

    const submitting = computed(() => store.getters.submitting);
    const hasError = computed(() => store.getters.hasError);
    const hasSuccess = computed(() => store.getters.hasSuccess);

    const formReadyToSubmit = computed(
      () =>
        !submitting.value &&
        formState.name.length > 0 &&
        formState.email.length > 0 &&
        formState.comment.length > 0
    );

    const onSubmitFormHandler = () => {
      store.dispatch("submitForm", formState);
    };

    return {
      formState,
      onSubmitFormHandler,
      submitting,
      hasError,
      hasSuccess,
      formReadyToSubmit,
      characterCount,
    };
  },
});

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:

console.log(this.name) // Getter

this.name = "Tim"; // Setter

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:

const currentStatus = computed({
  get: () => store.getters.getStatus,
  set: (val) => store.commit("SET_STATUS", val),
});

const resetFormHandler = () => {
  formState.name = "";
  formState.email = "";
  formState.comment = "";

  currentStatus.value = Status.IDLE;
};

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.

<template>
  <input v-model="inputValue">
</template>

<script>
export default {
  emits: [ 'update:modelValue' ],
  props: {
    modelValue: {
      type: String,
      default: ''
    }
  },
  computed: {
    inputValue: {
      get() {
        return this.modelValue;
      },
      set(val) {
        this.$emit('update:modelValue', val);
      }
    }
  }
}
</script>

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!

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

Nuxt DevTools v1.0: Redefining the Developer Experience Beyond Conventional Tools cover image

Nuxt DevTools v1.0: Redefining the Developer Experience Beyond Conventional Tools

In the ever-evolving world of web development, Nuxt.js has taken a monumental leap with the launch of Nuxt DevTools v1.0. More than just a set of tools, it's a game-changer—a faithful companion for developers. This groundbreaking release, available for all Nuxt projects and being defaulted from Nuxt v3.8 onwards, marks the beginning of a new era in developer tools. It's designed to simplify our development journey, offering unparalleled transparency, performance, and ease of use. Join me as we explore how Nuxt DevTools v1.0 is set to revolutionize our workflow, making development faster and more efficient than ever. What makes Nuxt DevTools so unique? Alright, let's start delving into the features that make this tool so amazing and unique. There are a lot, so buckle up! In-App DevTools The first thing that caught my attention is that breaking away from traditional browser extensions, Nuxt DevTools v1.0 is seamlessly integrated within your Nuxt app. This ensures universal compatibility across browsers and devices, offering a more stable and consistent development experience. This setup also means the tools are readily available in the app, making your work more efficient. It's a smart move from the usual browser extensions, making it a notable highlight. To use it you just need to press Shift + Option + D` (macOS) or `Shift + Alt + D` (Windows): With simple keystrokes, the Nuxt DevTools v1.0 springs to life directly within your app, ready for action. This integration eliminates the need to toggle between windows or panels, keeping your workflow streamlined and focused. The tools are not only easily accessible but also intelligently designed to enhance your productivity. Pages, Components, and Componsables View The Pages, Components, and Composables View in Nuxt DevTools v1.0 are a clear roadmap for your app. They help you understand how your app is built by simply showing its structure. It's like having a map that makes sense of your app's layout, making the complex parts of your code easier to understand. This is really helpful for new developers learning about the app and experienced developers working on big projects. Pages View lists all your app's pages, making it easier to move around and see how your site is structured. What's impressive is the live update capability. As you explore the DevTools, you can see the changes happening in real-time, giving you instant feedback on your app's behavior. Components View is like a detailed map of all the parts (components) your app uses, showing you how they connect and depend on each other. This helps you keep everything organized, especially in big projects. You can inspect components, change layouts, see their references, and filter them. By showcasing all the auto-imported composables, Nuxt DevTools provides a clear overview of the composables in use, including their source files. This feature brings much-needed clarity to managing composables within large projects. You can also see short descriptions and documentation links in some of them. Together, these features give you a clear picture of your app's layout and workings, simplifying navigation and management. Modules and Static Assets Management This aspect of the DevTools revolutionizes module management. It displays all registered modules, documentation, and repository links, making it easy to discover and install new modules from the community! This makes managing and expanding your app's capabilities more straightforward than ever. On the other hand, handling static assets like images and videos becomes a breeze. The tool allows you to preview and integrate these assets effortlessly within the DevTools environment. These features significantly enhance the ease and efficiency of managing your app's dynamic and static elements. The Runtime Config and Payload Editor The Runtime Config and Payload Editor in Nuxt DevTools make working with your app's settings and data straightforward. The Runtime Config lets you play with different configuration settings in real time, like adjusting settings on the fly and seeing the effects immediately. This is great for fine-tuning your app without guesswork. The Payload Editor is all about managing the data your app handles, especially data passed from server to client. It's like having a direct view and control over the data your app uses and displays. This tool is handy for seeing how changes in data impact your app, making it easier to understand and debug data-related issues. Open Graph Preview The Open Graph Preview in Nuxt DevTools is a feature I find incredibly handy and a real time-saver. It lets you see how your app will appear when shared on social media platforms. This tool is crucial for SEO and social media presence, as it previews the Open Graph tags (like images and descriptions) used when your app is shared. No more deploying first to check if everything looks right – you can now tweak and get instant feedback within the DevTools. This feature not only streamlines the process of optimizing for social media but also ensures your app makes the best possible first impression online. Timeline The Timeline feature in Nuxt DevTools is another standout tool. It lets you track when and how each part of your app (like composables) is called. This is different from typical performance tools because it focuses on the high-level aspects of your app, like navigation events and composable calls, giving you a more practical view of your app's operation. It's particularly useful for understanding the sequence and impact of events and actions in your app, making it easier to spot issues and optimize performance. This timeline view brings a new level of clarity to monitoring your app's behavior in real-time. Production Build Analyzer The Production Build Analyzer feature in Nuxt DevTools v1.0 is like a health check for your app. It looks at your app's final build and shows you how to make it better and faster. Think of it as a doctor for your app, pointing out areas that need improvement and helping you optimize performance. API Playground The API Playground in Nuxt DevTools v1.0 is like a sandbox where you can play and experiment with your app's APIs. It's a space where you can easily test and try out different things without affecting your main app. This makes it a great tool for trying out new ideas or checking how changes might work. Some other cool features Another amazing aspect of Nuxt DevTools is the embedded full-featured VS Code. It's like having your favorite code editor inside the DevTools, with all its powerful features and extensions. It's incredibly convenient for making quick edits or tweaks to your code. Then there's the Component Inspector. Think of it as your code's detective tool. It lets you easily pinpoint and understand which parts of your code are behind specific elements on your page. This makes identifying and editing components a breeze. And remember customization! Nuxt DevTools lets you tweak its UI to suit your style. This means you can set up the tools just how you like them, making your development environment more comfortable and tailored to your preferences. Conclusion In summary, Nuxt DevTools v1.0 marks a revolutionary step in web development, offering a comprehensive suite of features that elevate the entire development process. Features like live updates, easy navigation, and a user-friendly interface enrich the development experience. Each tool within Nuxt DevTools v1.0 is thoughtfully designed to simplify and enhance how developers build and manage their applications. In essence, Nuxt DevTools v1.0 is more than just a toolkit; it's a transformative companion for developers seeking to build high-quality web applications more efficiently and effectively. It represents the future of web development tools, setting new standards in developer experience and productivity....

Understanding Vue's Reactive Data cover image

Understanding Vue's Reactive Data

Introduction Web development has always been about creating dynamic experiences. One of the biggest challenges developers face is managing how data changes over time and reflecting these changes in the UI promptly and accurately. This is where Vue.js, one of the most popular JavaScript frameworks, excels with its powerful reactive data system. In this article, we dig into the heart of Vue's reactivity system. We unravel how it perfectly syncs your application UI with the underlying data state, allowing for a seamless user experience. Whether new to Vue or looking to deepen your understanding, this guide will provide a clear and concise overview of Vue's reactivity, empowering you to build more efficient and responsive Vue 3 applications. So, let’s kick off and embark on this journey to decode Vue's reactive data system. What is Vue's Reactive Data? What does it mean for data to be ”'reactive”? In essence, when data is reactive, it means that every time the data changes, all parts of the UI that rely on this data automatically update to reflect these changes. This ensures that the user is always looking at the most current state of the application. At its core, Vue's Reactive Data is like a superpower for your application data. Think of it like a mirror - whatever changes you make in your data, the user interface (UI) reflects these changes instantly, like a mirror reflecting your image. This automatic update feature is what we refer to as “reactivity”. To visualize this concept, let's use an example of a simple Vue application displaying a message on the screen: `javascript import { createApp, reactive } from 'vue'; const app = createApp({ setup() { const state = reactive({ message: 'Hello Vue!' }); return { state }; } }); app.mount('#app'); ` In this application, 'message' is a piece of data that says 'Hello Vue!'. Let's say you change this message to 'Goodbye Vue!' later in your code, like when a button is clicked. `javascript state.message = 'Goodbye Vue!'; ` With Vue's reactivity, when you change your data, the UI automatically updates to 'Goodbye Vue!' instead of 'Hello Vue!'. You don't have to write extra code to make this update happen - Vue's Reactive Data system takes care of it. How does it work? Let's keep the mirror example going. Vue's Reactive Data is the mirror that reflects your data changes in the UI. But how does this mirror know when and what to reflect? That's where Vue's underlying mechanism comes into play. Vue has a behind-the-scenes mechanism that helps it stay alerted to any changes in your data. When you create a reactive data object, Vue doesn't just leave it as it is. Instead, it sends this data object through a transformation process and wraps it up in a Proxy. Proxy objects are powerful and can detect when a property is changed, updated, or deleted. Let's use our previous example: `javascript import { createApp, reactive } from 'vue'; const app = createApp({ setup() { const state = reactive({ message: 'Hello Vue!' }); return { state }; } }); app.mount('#app'); ` Consider our “message” data as a book in a library. Vue places this book (our data) within a special book cover (the Proxy). This book cover is unique - it's embedded with a tracking device that notifies Vue every time someone reads the book (accesses the data) or annotates a page (changes the data). In our example, the reactive function creates a Proxy object that wraps around our state object. When you change the 'message': `javascript state.message = 'Goodbye Vue!'; ` The Proxy notices this (like a built-in alarm going off) and alerts Vue that something has changed. Vue then updates the UI to reflect this change. Let’s look deeper into what Vue is doing for us and how it transforms our object into a Proxy object. You don't have to worry about creating or managing the Proxy; Vue handles everything. `javascript const state = reactive({ message: 'Hello Vue!' }); // What vue is doing behind the scenes: function reactive(obj) { return new Proxy(obj, { // target = state and key = message get(target, key) { track(target, key) return target[key] }, set(target, key, value) { target[key] = value // Here Vue will trigger its reactivity system to update the DOM. trigger(target, key) } }) } ` In the example above, we encapsulate our object, in this case, “state”, converting it into a Proxy object. Note that within the second argument of the Proxy, we have two methods: a getter and a setter. The getter method is straightforward: it merely returns the value, which in this instance is “state.message” equating to 'Hello Vue!' Meanwhile, the setter method comes into play when a new value is assigned, as in the case of “state.message = ‘Hey young padawan!’”. Here, “value” becomes our new 'Hey young padawan!', prompting the property to update. This action, in turn, triggers the reactivity system, which subsequently updates the DOM. Venturing Further into the Depths If you have been paying attention to our examples above, you might have noticed that inside the Proxy` method, we call the functions `track` and `trigger` to run our reactivity. Let’s try to understand a bit more about them. You see, Vue 3 reactivity data is more about Proxy objects. Let’s create a new example: `vue import { reactive, watch, computed, effect } from "vue"; const state = reactive({ showSword: false, message: "Hey young padawn!", }); function changeMessage() { state.message = "It's dangerous to go alone! Take this."; } effect(() => { if (state.message === "It's dangerous to go alone! Take this.") { state.showSword = true; } }); {{ state.message }} Click! ` In this example, when you click on the button, the message's value changes. This change triggers the effect function to run, as it's actively listening for any changes in its dependencies__. How does the effect` property know when to be called? Vue 3 has three main functions to run our reactivity: effect`, `track`, and `trigger`. The effect` function is like our supervisor. It steps in and takes action when our data changes – similar to our effect method, we will dive in more later. Next, we have the track` function. It notes down all the important data we need to keep an eye on. In our case, this data would be `state.message`. Lastly, we've got the trigger` function. This one is like our alarm bell. It alerts the `effect` function whenever our important data (the stuff `track` is keeping an eye on) changes. In this way, trigger`, `track`, and `effect` work together to keep our Vue application reacting smoothly to changes in data. Let’s go back to them: `javascript function reactive(obj) { return new Proxy(obj, { get(target, key) { // target = state & key = message track(target, key) // keep an eye for this return target[key] }, set(target, key, value) { target[key] = value trigger(target, key) // trigger the effects! } }) } ` Tracking (Dependency Collection) Tracking is the process of registering dependencies between reactive objects and the effects that depend on them. When a reactive property is read, it's "tracked" as a dependency of the current running effect. When we execute track()`, we essentially store our effects in a Set object. But what exactly is an "effect"? If we revisit our previous example, we see that the effect method must be run whenever any property changes. This action — running the effect method in response to property changes — is what we refer to as an "Effect"! (computed property, watcher, etc.) > Note: We'll outline a basic, high-level overview of what might happen under the hood. Please note that the actual implementation is more complex and optimized, but this should give you an idea of how it works. Let’s see how it works! In our example, we have the following reactive object: `javascript const state = reactive({ showSword: false, message: "Hey young padawn!", }); // which is transformed under the hood to: function reactive(obj) { return new Proxy(obj, { get(target, key) { // target = state | key = message track(target, key) // keep an eye for this return target[key] }, set(target, key, value) { target[key] = value trigger(target, key) // trigger the effects! } }) } ` We need a way to reference the reactive object with its effects. For that, we use a WeakMap. Which type is going to look something like this: `typescript WeakMap>> ` We are using a WeakMap to set our object state as the target (or key). In the Vue code, they call this object `targetMap`. Within this targetMap` object, our value is an object named `depMap` of Map type. Here, the keys represent our properties (in our case, that would be `message` and `showSword`), and the values correspond to their effects – remember, they are stored in a Set that in Vue 3 we refer to as `dep`. Huh… It might seem a bit complex, right? Let's make it more straightforward with a visual example: With the above explained, let’s see what this Track` method kind of looks like and how it uses this `targetMap`. This method essentially is doing something like this: `javascript let activeEffect; // we will see more of this later function track(target, key) { if (activeEffect) { // depsMap` maps targets to their keys and dependent effects let depsMap = targetMap.get(target); // If we don't have a depsMap for this target in our targetMap`, create one. if (!depsMap) { depsMap = new Map(); targetMap.set(target, depsMap); } let dep = depsMap.get(key); if (!dep) { // If we don't have a set of effects for this key in our depsMap`, create one. dep = new Set(); depsMap.set(key, dep); } // Add the current effect as a dependency dep.add(activeEffect); } } ` At this point, you have to be wondering, how does Vue 3 know what activeEffect` should run? Vue 3 keeps track of the currently running effect by using a global variable. When an effect is executed, Vue temporarily stores a reference to it in this global variable, allowing the track` function to access the currently running effect and associate it with the accessed reactive property. This global variable is called inside Vue as `activeEffect`. Vue 3 knows which effect is assigned to this global variable by wrapping the effects functions in a method that invokes the effect whenever a dependency changes. And yes, you guessed, that method is our effect` method. `javascript effect(() => { if (state.message === "It's dangerous to go alone! Take this.") { state.showSword = true; } }); ` This method behind the scenes is doing something similar to this: `javascript function effect(update) { //the function we are passing in const effectMethod = () => { // Assign the effect as our activeEffect` activeEffect = effectMethod // Runs the actual method, also triggering the get` trap inside our proxy update(); // Clean the activeEffect after our Effect has finished activeEffect = null } effectMethod() } ` The handling of activeEffect` within Vue's reactivity system is a dance of careful timing, scoping, and context preservation. Let’s go step by step on how this is working all together. When we run our `Effect` method for the first time, we call the `get` trap of the Proxy. `javascript function effect(update) const effectMethod = () => { // Storing our active effect activeEffect = effectMethod // Running the effect update() ... } ... } effect(() => { // we call the the get` trap when getting our `state.message` if (state.message === "It's dangerous to go alone! Take this.") { state.showSword = true; } }); ` When running the get` trap, we have our `activeEffect` so we can store it as a dependency. `javascript function reactive(obj) { return new Proxy(obj, { // Gets called when our effect runs get(target, key) { track(target, key) // Saves the effect return target[key] }, // ... (other handlers) }) } function track(target, key) { if (activeEffect) { //... rest of the code // Add the current effect as a dependency dep.add(activeEffect); } } ` This coordination ensures that when a reactive property is accessed within an effect, the track function knows which effect is responsible for that access. Trigger Method Our last method makes this Reactive system to be complete. The trigger` method looks up the dependencies for the given target and key and re-runs all dependent effects. `javascript function trigger(target, key) { const depsMap = targetMap.get(target); if (!depsMap) return; // no dependencies, no effects, no need to do anything const dep = depsMap.get(key); if (!dep) return; // no dependencies for this key, no need to do anything // all dependent effects to be re-run dep.forEach(effect => { effect() }); } ` Conclusion Diving into Vue 3's reactivity system has been like unlocking a hidden superpower in my web development toolkit, and honestly, I've had a blast learning about it. From the rudimentary elements of reactive data and instantaneous UI updates to the intricate details involving Proxies, track and trigger functions, and effects, Vue 3's reactivity is an impressively robust framework for building dynamic and responsive applications. In our journey through Vue 3's reactivity, we've uncovered how this framework ensures real-time and precise updates to the UI. We've delved into the use of Proxies to intercept and monitor variable changes and dissected the roles of track and trigger functions, along with the 'effect' method, in facilitating seamless UI updates. Along the way, we've also discovered how Vue ingeniously manages data dependencies through sophisticated data structures like WeakMaps and Sets, offering us a glimpse into its efficient approach to change detection and UI rendering. Whether you're just starting with Vue 3 or an experienced developer looking to level up, understanding this reactivity system is a game-changer. It doesn't just streamline the development process; it enables you to create more interactive, scalable, and maintainable applications. I love Vue 3, and mastering its reactivity system has been enlightening and fun. Thanks for reading, and as always, happy coding!...

Styling Vue Single-File Components cover image

Styling Vue Single-File Components

Introduction If you have any experience with writing Vue single-file components, you have probably spent some time writing CSS within your component. Single-File Components allow developers to group code together in more logical ways, rather than breaking up components by language utilized (HTML, CSS, or JavaScript). Being able to group component styles directly next to the HTML that it applies to is one of the major benefits of Vue, including the ability to scope CSS to the component so that it doesn't affect other parts of the UI. However, there are a number of features to Vue's CSS integration that you may not be familar with, such as applying styles directly to slotted elements, or the newest features available in Vue 3.2. Let's explore some of these other ways of styling Vue single-file components, and how they can benefit your applications. Scoped Styles Let's start with the most common usage of CSS in Vue: scoped styles. One of the difficulties on writing modern applications is that our CSS files begin to grow larger and larger, until nobody really knows where certain styles are used or what a given change might affect. This can lead to copying certain CSS selectors, and simply duplicating them for each component. There are other solutions for this (such as BEM or utility classes), but when working with a component-based framework like Vue, it makes a lot of sense to group CSS classes within the component. Scoped styles allows us to write CSS that only applies to the component we are working in. Here's an example from the Vue docs: `html .example { color: red; } hi ` With this, the example` class will only ever apply within this component. This is achieved by added a unique data attribute to all elements within the component, so the normal CSS cascade is still applied. External styles can still impact the design of this component, but its scoped styles cannot leak out to other components. Deep Styles This leads to an interesting problem. If our component's styles are scoped, what about children components? By default, they would not receive any styling from our scoped styles. However, Vue provides a way to do that. Let's look at an example below. `html Card Title Lorum ipsum dolor sit amet header :deep(.card-title) { font-weight: bold; } section { padding 2rem; } Title ` By using the :deep()` pseudo class, we are able to tell Vue that this particular class (`.card-title`) should not be scoped. Because the special ID is still applied to the root element (`header`), the style is still scoped, but it is available for any child component beneath it. Slotted Styles A problem I've run into in many situations is that I have a component being injected with slots, but I cannot control the styling of it like I want. Vue provides a solution for this as well with slotted styles. Let's review the above example, but this time we'll add a slotted style to our Title.vue component. `html Card Title Lorum ipsum dolor sit amet header :deep(.card-title) { font-weight: bold; } section { padding 2rem; } Title :slotted(h1) { font-size: 3rem; } ` Here, we added the :slotted` pseudo class, so that any slotted `h1` tags have the correct style applied to them. This may be a contrived example, but consider needing to have different header styles for each header tag (or equivalent CSS class). The Title.vue component can manage all of these styles, rather than relying on the consumer of these components to pass in the correct class or styling. Global Styles Of course, sometimes you need to apply styles globally, even within a scoped component. Vue provides us with two different ways to handle this: the :global` pseudo selector and multiple style blocks. `:global` Within a scoped style block, if you only need to provide one class as a global value, you can use the :global` pseudo selector to note that the style should not be scoped. From the Vue docs: `html :global(.red) { color: red; } ` Multiple style blocks There is also nothing stopping you from having multiple style blocks within your Vue component. Simply create another ` tag, and put your global styles in there. `html / global styles */ / local styles */ ` Style Modules If you're coming from React, you may be more familiar with CSS modules, where you import a CSS file and access its classes as a JavaScript object. The same can be done within Vue by using ` instead of ``. Here's an example from the Vue docs: `html This should be red .red { color: red; } ` This can be particularly nice to work with, so that you aren't passing strings around in your classes (which are prone to errors and typos). Vue also allows you to rename what the object is, so that you don't need to access them with $style` in your template if you don't want to. Dynamic CSS Values The latest feature in Vue is state-driven dynamic CSS values. There is a trend in modern CSS to use custom properties as a way to dynamically update the value of some CSS property. This can allow our CSS to be more flexible, and interact nicely with our other application code. Let's look at an example component that renders a progress bar: `html Progress {{ progress }}% import { watch } from 'vue' const props = defineProps({ progress: { type: Number, required: true } }) watch(props.progress, (value) => document .documentElement .style .setProperty('--complete-percentage', value + '%'), { immediate: true }) .progress-bar { background-color: #ccc; border-radius: 13px; padding: 3px; } .progress-bar > div { background-color: #000; width: var(--complete-percentage); height: 8px; border-radius: 10px; transition-property: width; transition-duration: 150ms; } ` This component takes in a number (progress`), then both displays that number and updates a CSS custom property with the value. As the progress changes, the CSS property is continually updated to stay in sync with the JavaScript value. In Vue 3.2, however, we are provided with a special CSS function that does this whole thing for us! Take a look at the updated code: `html Progress {{ progress }}% const props = defineProps({ progress: { type: Number, required: true } }) .progress-bar { background-color: #ccc; border-radius: 13px; padding: 3px; } .progress-bar > div { background-color: #000; width: v-bind(props.progress); height: 8px; border-radius: 10px; transition-property: width; transition-duration: 150ms; } ` By using v-bind(props.progress)`, we have elimited the need for our custom watcher, and it's clear that our CSS is being kept in sync with the value of `props.progress`. Under the hood, Vue is doing the same thing for us with a custom property, but it's so much nicer than having to write it ourselves. Conclusion CSS is a complicated language in practice, and mixing it with JavaScript makes things even more complex. Vue provides developers with the tools to handle CSS in a reliable and predictable way, which encourages building in a component-based architecture. Next time you're running into trouble with CSS in Vue, see if one of these techniques can be useful to you!...

Software Team Leadership: Risk Taking & Decision Making with David Cramer, Co-Founder & CTO at Sentry cover image

Software Team Leadership: Risk Taking & Decision Making with David Cramer, Co-Founder & CTO at Sentry

In this episode of the engineering leadership series, Rob Ocel interviews David Cramer, co-founder and CTO of Sentry, delving into the importance of decision-making, risk-taking, and the challenges faced in the software engineering industry. David emphasizes the significance of having conviction and being willing to make decisions, even if they turn out to be wrong. He shares his experience of attending a CEO event, where he discovered that decision-making and conflict resolution are struggles even for successful individuals. David highlights the importance of making decisions quickly and accepting the associated risks, rather than attempting to pursue multiple options simultaneously. He believes that being decisive is crucial in the fast-paced software engineering industry. This approach allows for faster progress and adaptation, even if it means occasionally making mistakes along the way. The success of Sentry is attributed to a combination of factors, including market opportunity and the team's principles and conviction. David acknowledges that bold ideas often carry a higher risk of failure, but if they do succeed, the outcome can be incredibly significant. This mindset has contributed to Sentry’s achievements in the industry. The interview also touches on the challenges of developing and defending opinions in the software engineering field. David acknowledges that it can be difficult to navigate differing viewpoints and conflicting ideas. However, he emphasizes the importance of standing by one's convictions and being open to constructive criticism and feedback. Throughout the conversation, David emphasizes the need for engineering leaders to be decisive and take calculated risks. He encourages leaders to trust their instincts and make decisions promptly, even if they are uncertain about the outcome. This approach fosters a culture of innovation and progress within engineering teams. The episode provides valuable insights into the decision-making process and the challenges faced by engineering leaders. It highlights the importance of conviction, risk-taking, and the ability to make decisions quickly in the software engineering industry. David's experiences and perspectives offer valuable lessons for aspiring engineering leaders looking to navigate the complexities of the field....