Skip to content

Provide/Inject API With Vue 3

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Introduction

One of the most difficult problems to solve when building single-page applications is state management. With component-based frameworks like Vue, this is typically solved in one of two ways:

  1. State is managed by components, with data being passed to child components as props. The parent component then listens for events and performs actions on the state accordingly.
  2. State is managed with a global state management library (Vuex, Redux, etc). Global state is then injected into the desired components, and those components triggers actions in the state management library (such as API requests or data updates). This can provide a layer of separation between logic and templating which is useful, and helps with passing data between parts of the application.

Vue offers an interesting middleground between these two approaches with the Provide/Inject API. This API is similar to React's Context API, in that it allows a component to provide some sort of data to any component beneath it in the component tree. For example, a parent component could provide a piece of data (say, the user's username), and then a grandchild component could inject that value into itself.

Provide/Inject gives developers a way to share data between parent and child components while avoiding prop drilling (passing a prop from one component to the next in a chain). This can make your code more readable, and reduce the complexity of your props for any components that don't rely on this data.

In this article, we will explore a basic example using the Provide/Inject API, building up a new application with a user dashboard to update their name and email.

Getting Started - Using Props

We'll first set up our example application using the standard approach to passing data between components - props. Below is our homepage and a navigation page.

<!-- App.vue -->
<template>
  <div class="w-2/3 pt-6 m-auto">
    <Nav :name="state.name" />
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive } from "vue";
import Nav from "./components/Nav.vue";

export default defineComponent({
  setup() {
    const state = reactive({
      name: "Bob Day",
      email: "bob@martianmovers.com",
    });

    return { state };
  },
  components: {
    Nav,
  },
});
</script>
<!-- Nav.vue -->
<template>
  <nav class="flex">
    <div class="flex-grow">
      <a class="px-4 hover:underline" href="#">Home</a>
      <a class="px-4 hover:underline" href="#">About</a>
      <a class="px-4 hover:underline" href="#">My Account</a>
    </div>
    <div class="flex-shrink">
      Hello, {{ name }}!
    </div>
  </nav>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    name: {
      type: String,
      default: "User"
    }
  },
  setup(props) {
    return {
      ...props
    }
  },
})
</script>

This is a very straightforward parent/child structure. Our parent component has a reactive object (state) with a user's name and email. The username is passed into the Nav component as a prop, and then displayed. This works well because we only have two components, but what if there were other layout components between the root and the navigation component?

We can use Provide/Inject to send this data from the parent to the navigation component. In our parent component, we will provide the data we want available (the username), and then inject that data into the Nav component.

Provide/Inject with Composition API

Let's start with App.vue, and explore how to use provide. Below is our rewritten root component:

<!-- App.vue -->
<template>
  <div class="w-2/3 pt-6 m-auto">
    <Nav />
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, provide, reactive } from "vue";
import Nav from "./components/Nav.vue";

export default defineComponent({
  setup() {
    const state = reactive({
      name: "Bob Day",
      email: "bob@martianmovers.com",
    });

    provide('username', computed(() => state.name));

    return { state };
  },
  components: {
    Nav,
  },
});
</script>

With Vue 3, we have access to a Composition API method provide. This function takes a key and a value. The key is how the provided value will be accessed in other components. In this example. we are passing a computed property with the user's username as the value.

Why are we passing a computed property? By default, the provided value is not reactive. If we just wrote provide('username', state.name), any component that injected this value would only have the initial state of the username. If it were to change in the future, the name would be out of sync with the root component. If we wanted to provide the entire state, we could write this instead:

provide('state', state);

That's because we're using a reactive object for our state. Alternatively, if the username was a ref, we could also use that in the same way.

Keep in mind that any value could be passed as an argument to provide, including functions. This will come up later in our example, but it's important to think about when you're wanting to use this API.

Let's look our our navigation component now, using inject to get the value of state.name.

<!-- Nav.vue -->
<template>
  <nav class="flex">
    <div class="flex-grow">
      <a class="px-4 hover:underline" href="#">Home</a>
      <a class="px-4 hover:underline" href="#">About</a>
      <a class="px-4 hover:underline" href="#">My Account</a>
    </div>
    <div class="flex-shrink">
      Hello, {{ name }}!
    </div>
  </nav>
</template>

<script lang="ts">
import { defineComponent, inject } from 'vue'
export default defineComponent({
  setup() {
    const name = inject('username');

    return {
      name
    }
  },
})
</script>

Similar to what we did in App.vue, we used a Composition API method called inject to get the value. inject takes the key we used when providing the data, and then returns the value as a variable. Since we provided a computed property, inject returns a computed property as well.

inject has two other arguments as well:

  1. defaultValue: This is the value that should be returned in the event a provided value is not found with that key.
  2. treatDefaultAsFactory: As I noted above, any value (including functions) can be provided as a value to inject. In the event that a function is what you are providing, you don't want it to be invoked by mistake. But what if you're providing an object? Unless it is returned from a function, you could end up with duplicate objects that have the same reference (this is also why data and props recommend returning objects from a function rather than setting them directly). When using a function as the default value, this argument tells inject whether the default is the function itself, or the value returned by the function.

The two below examples return the same default, a string:

// With a default value
const name = inject('usernafme', 'Bob Day');

// With a default factory
const name = inject('usernafme', () => 'Bob Day', true);

In our component example, the username is being injected and returned from setup, making it available in the template. We can then use that variable as we normally would, but without having to worry about props. Nice! This could save us a lot of time and effort with prop drilling.

Providing Reactivity

In the last section, we discussed reactivity and how the argument in provide needs to be a reactive object if we want data to stay in sync. Let's build out our user dashboard so they can update their name and email. In our App.vue, we're going to add a single line to our setup method:

provide('userDetails', state);

This will provide the entire state object (a reactive object) to whatever component wants to inject it. Now, let's build out a dashboard to work with that data:

<!-- MyProfile.vue -->
<template>
  <div class="flex flex-col">
    <h2 class="block m-auto text-2xl">My Profile</h2>
    <hr />
    <label class="py-1 flex">
      <span class="w-24">Username: </span>
      <input class="shadow p-1 bg-gray-100 w-64" v-model="userDetails.name" />
    </label>
    <label class="py-1 flex">
      <span class="w-24">Email:</span>
      <input
        class="shadow p-1 bg-gray-100 w-64"
        type="email"
        v-model="userDetails.email"
      />
    </label>
  </div>
</template>

<script lang="ts">
import { defineComponent, inject } from "vue";

export default defineComponent({
  setup() {
    const userDetails = inject("userDetails");

    return {
      userDetails,
    };
  },
});
</script>

In this component, we inject the entire userDetails provided value, and return it to the template. We can then use v-model to bind directly to the injected values. With this in place, it all works as expected! Any changes made to the username field would properly update in the navigation as well.

However, there's a small catch to doing things this way. Per the Vue 3 documentation, "When using reactive provide / inject values, it is recommended to keep any mutations to reactive properties inside of the provider whenever possible." The reason for this is that allowing any child component to mutate a value could lead to confusion about where a particular mutation is happening. The more disciplined our codebase is about mutating state, the more stable and predictable it will be.

Rather than directly using v-model on our reactive state, let's provide a couple functions that will do the updating for us. First, we'll update App.vue to handle the new providers:

<!-- App.vue -->
<template>
  <div class="w-2/3 pt-6 m-auto">
    <Nav />
    <main class="py-6">
      <MyProfile />
    </main>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, provide, reactive } from "vue";
import Nav from "./components/Nav.vue";
import MyProfile from "./components/MyProfile.vue";

export default defineComponent({
  setup() {
    const state = reactive({
      name: "Bob Day",
      email: "bob@martianmovers.com",
    });

    const updateUsername = (name) => {
      state.name = name;
    };

    const updateEmail = (email) => {
      state.email = email;
    };

    provide(
      "username",
      computed(() => state.name)
    );
    provide("userDetails", state);
    provide("updateUsername", updateUsername);
    provide("updateEmail", updateEmail);

    return { state };
  },
  components: {
    Nav,
    MyProfile,
  },
});
</script>

We have added two functions - updateUsername and updateEmail. These functions are nearly identical, just updating the value on our state object that they are associated to. We then provide these two functions using the provide method, so that they are available to children components.

Remember above when we discussed that any value could be provided? This is why treatDefaultAsFactory is important. Here, we are providing two functions that don't return anything. If inject by default invoked the function and returns its value, we would get undefined is not a function errors in our child component. In this case, we're really wanting a function to be injected into our component, so defaulting treatDefaultAsFactory to false is excellent.

Here's the updated code for MyProfile.vue:

<!-- MyProfile.vue -->
<template>
  <div class="flex flex-col">
    <h2 class="block m-auto text-2xl">My Profile</h2>
    <hr />
    <label class="py-1 flex">
      <span class="w-24">Username: </span>
      <input class="shadow p-1 bg-gray-100 w-64" v-model="username" />
    </label>
    <label class="py-1 flex">
      <span class="w-24">Email:</span>
      <input class="shadow p-1 bg-gray-100 w-64" type="email" v-model="email" />
    </label>
  </div>
</template>

<script>
import { defineComponent, inject, computed } from "vue";

export default defineComponent({
  setup() {
    const userDetails = inject("userDetails");
    const updateUsername = inject("updateUsername");
    const updateEmail = inject("updateEmail");

    const username = computed({
      get: () => userDetails.name,
      set: updateUsername,
    });

    const email = computed({
      get: () => userDetails.email,
      set: updateEmail,
    });

    return {
      username,
      email,
    };
  },
});
</script>

Rather than binding directly on userDetails, we created two computed properties, each with a getter and setter. We can then bind to the computed properties, since they will return the desired value (username or email) and trigger the update methods we injected. Now our reactivity is fully controlled by the root component, App.vue, rather than the child.

Global State Management

I mentioned above that Provide/Inject gives us a middle ground between global state and component state. With the introduction of the Composition API, however, there's no reason we can't use Provide/Inject as our global management. Let's take everything we've written so far and extract it to a separate file:

import { computed, inject, provide, reactive } from "vue";

export const initStore = () => {
  // State
  const state = reactive({
    name: "Bob Day",
    email: "bob@martianmovers.com",
  });

  // Getters
  const getUsername = computed(() => state.name);
  const getEmail = computed(() => state.email);

  // Actions
  const updateUsername = (name) => {
    state.name = name;
  };
  const updateEmail = (email) => {
    state.email = email;
  };

  provide("getUsername", getUsername);
  provide("getEmail", getEmail);
  provide("updateUsername", updateUsername);
  provide("updateEmail", updateEmail);
};

export const useStore = () => ({
  getUsername: inject("getUsername"),
  getEmail: inject("getEmail"),
  updateUsername: inject("updateUsername"),
  updateEmail: inject("updateEmail"),
});

In this file, we have two functions - initStore and useStore. initStore creates our reactive object, getters for both the username and email, and methods to perform updates, then provides each of those values. These three groups (state, computed, and methods) maps very nicely to how Vuex works (state, getters, and actions).

The second method, useStore, simply returns an object with the injected values. This lets us use the store we've created from a single location, so if we change the key used in provide, we can also update it in the inject. This ensures we aren't duplicating our inject calls, and we only have one file to check if something goes wrong.

Our App.vue file is now a lot simpler:

import { defineComponent } from "vue";
import { initStore } from "./store/store";
import Nav from "./components/Nav.vue";
import MyProfile from "./components/MyProfile.vue";

export default defineComponent({
  setup() {
    initStore();
  },
  components: {
    Nav,
    MyProfile,
  },
});

Since we don't need the store values in our root component, we can safely call initStore to generate the store, and provide its values to our child components. Then, in MyProfile.vue, we can do the following:

import { defineComponent, computed } from "vue";
import { useStore } from "../store/store";

export default defineComponent({
  setup() {
    const store = useStore();

    const username = computed({
      get: () => store.getUsername.value,
      set: store.updateUsername,
    });

    const email = computed({
      get: () => store.getEmail.value,
      set: store.updateEmail,
    });

    return {
      username,
      email,
    };
  },
});

Because useStore injects the values for us, we have access to the username, password, and their update methods. This is one of the ways that the Composition API can help keep our code clean, and our logic bundled by feature rather than functionality.

If this concept interests you, there's a library for Vue 2 and 3 called Pinia that takes this approach to the next level. Pinia provides you a typesafe, easy to maintain global store. Check it out!

Conclusion

Using Provide/Inject can help remove some of the complexity of passing data between parent and child components. Keep in mind that provided values do not have the same checks as props, such as required or type, so they are inherently less safe to use. There is no guarantee that the value you want to inject is present in the component tree, nor do you know for certain what shape that value is in.

Also, up until recently, the Vue documentation included, "provide and inject are primarily provided for advanced plugin / component library use cases. It is NOT recommended to use them in generic application code." This has since been removed, and libraries like Pinia show the power of using this API in application code. I would still recommend being careful when choosing to implement a feature using Provide and Inject. That said, have fun and try it out!

Here's a link to a Stackblitz example of the final form of the appliation we worked through above.

Until next time!

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

Awesome 3D experience with VueJS and TresJS: a beginner's guide cover image

Awesome 3D experience with VueJS and TresJS: a beginner's guide

Awesome 3D experience with VueJS and TresJS: a beginner's guide Vue.js developers are renowned for raving about the ease, flexibility, and speed of development their framework offers. Tres.js builds on this love for Vue by becoming the missing piece for seamless 3D integration. As a Vue layer for Three.js, Tres.js allows you to leverage the power of Three.js, a popular 3D library, within the familiar and beloved world of Vue components. This means you can create stunning 3D graphics and animations directly within your Vue applications, all while maintaining the clean and efficient workflow you've come to expect. TresJS is a library specifically designed to make incorporating WebGL (the web's 3D graphics API) into your Vue.js projects a breeze. It boasts several key features that make 3D development with Vue a joy: - Declarative Approach: Build your 3D scenes like you would any other Vue component, leveraging the power and familiarity of Vue's syntax. This makes it intuitive and easy to reason about your 3D elements. - Powered by Vite: Experience blazing-fast development cycles with Vite's Hot Module Replacement (HMR) that keeps your scenes updated in real-time, even as your code changes. - Up-to-date Features: Tres.js stays on top of the latest Three.js releases, ensuring you have immediate access to the newest features and functionality. - Thriving Ecosystem: The Tres.js ecosystem offers many resources to enhance your development experience. This includes: - Cientos: A collection of pre-built components and helpers that extend the capabilities of Tres.js, allowing you to focus on building your scene's functionality rather than reinventing the wheel (https://cientos.tresjs.org/). - TresLeches: A powerful state management solution specifically designed for 3D applications built with Tres.js (https://tresleches.tresjs.org/). You can try TresJS online using their official Playground or on their StackBlitz starter. But now, let's dive into a quick code example to showcase the simplicity of creating a 3D scene with TresJS. Setup First, install the package: npm install @tresjs/core three And then, if you are using Typescript, be sure to install the types: npm install @types/three -D If you are using Vite, now you need to modify your vite.config.ts file in this way to make the template compiler work with the custom renderer: ` Create our Scene Imagine a 3D scene as a virtual stage. To bring this stage to life, we need a few key players working together: 1. Scene: Think of this as the container that holds everything in your 3D world. It acts as the canvas where all the objects, lights, and the camera reside, defining the overall environment. 2. Renderer: This is the magician behind the curtain, responsible for taking all the elements in your scene and translating them into what you see on the screen. It performs the complex calculations needed to transform your 3D scene into 2D pixels displayed on your browser. 3. Camera: Like a real camera, this virtual camera defines the perspective from which you view your scene. You can position and adjust the camera to zoom in, zoom out, or explore different angles within your 3D world. - To make our camera dynamic and allow canvas exploration, we are going to leverage the client's OrbitControls component. Below are our examples. You will see that we just include the component in our canvas, and it just works. 4. Objects: These actors bring your scene to life. They can be anything from simple geometric shapes like spheres and cubes to complex models like characters or buildings. You create the visual elements that tell your story by manipulating and animating these objects. Starting from the beginning: to create our Scene with TresJS we just need to use our component TresCanvas in our Vue component's template: ` The TresCanvas component is going to do some setup work behind the scenes: - It creates a WebGLRenderer that automatically updates every frame. - It sets the render loop to be called on every frame based on the browser refresh rate. Using the window-size property, we force the canvas to take the width and height of our full window. So with TresCanvas component we have created our Renderer and our Scene. Let's move to the Camera: ` We just have to add the TresPerspectiveCamera component to our scene. NOTE: It's important that all scene-related components live between the TresCanvas component. Now, only the main actor is missing, let's add some styles and our object inside the scene. Our Vue component will now look like: ` And our scene will be: A Mesh is a basic scene object in three.js, and it's used to hold the geometry and the material needed to represent a shape in 3D space. As we can see, we can achieve the same with TresJS using the TresMesh component, and between the default slots, we are just passing our object (a Box in our example). One interesting thing to notice is that we don't need to import anything. That's because TresJS automatically generates a Vue Component based on the three objects you want to use in PascalCase with a Tres prefix. Now, if we want to add some color to our object the Three.js Material class comes to help us. We need to add: ` Conclusion Tres.js not only supercharges Vue.js applications with stunning 3D graphics, but it also integrates seamlessly with Nuxt.js, enabling you to harness the performance benefits of server-side rendering (SSR) for your 3D creations. This opens the door to building exceptional web experiences that are both interactive and performant. With Tres.js, Vue.js developers can leverage a declarative approach, cutting-edge features, and a vast ecosystem to bring their immersive web visions to life. If you want to elevate your Vue.js projects with a new dimension, Tres.js is an excellent choice to explore....

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: ` 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. ` 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: ` 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': ` 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. ` 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: ` 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: ` 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: ` 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: ` 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: ` 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. ` This method behind the scenes is doing something similar to this: ` 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. ` When running the get trap, we have our activeEffect so we can store it as a dependency. ` 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. ` 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!...

TC39 - How Changes are Made to JavaScript cover image

TC39 - How Changes are Made to JavaScript

Introduction The JavaScript ecosystem is constantly changing. As developers, we are very familiar with the ever-shifting landscape of frameworks, libraries, and tooling required to write our applications. In addition, there are other runtimes for Javascript beyond the browser, including Node, Deno, Cloudflare Workers, with more being released all the time. All of this - the tooling, the frameworks, the runtimes, even the language - are based on standards developed by a group of individuals and companies know as TC39. TC39 (Technical Committee 39) is a committee organized by Ecma International, a nonprofit standards organization for information and communication systems. In 1996, Netscape (the original creators of JavaScript) began meeting with Ecma to discuss standardizing the language. The first standard edition of JavaScript (called ECMAScript) was adopted in 1997, with further releases of the standard happening since then. The JavaScript we use today is an implementation of these standards, and each runtime of JavaScript works to implement them for use by developers. This standardization across runtimes was not always a guarantee, however. For a long time, the Node project tended to go its own way, implementing Node-specific APIs and methods of accomplishing development work. Many within Node originally felt that TC39 was forcing their standards on the Node project, despite Node havings its own needs and solutions. There are a number of examples where Node went one way, and the JavaScript standards went the other - Promises and imports are two good examples. However, the Node steering committee today is much more open to adopting standards, any many of its members participate in discussions with TC39 regarding new features and changes to JavaScript. This is in part because developers want the same language and APIs in both the browser and their Node environments, but also, because there are other runtimes to consider when developing JavaScript code. This standardization has brought about a number of changes to the language and the JS ecosystem, as more voices are coming together to work on new solution to existing problems. What does TC39 do? As I mentioned, TC39 is a committee focused on developing and ensuring the JavaScript standard. From their website, "Ecma International's TC39 is a group of JavaScript developers, implementers, academics, and more, collaborating with the community to maintain and evolve the definition of JavaScript." The committee takes proposals from the community, and determines which are going to be worked on to be implemented in the JavaScript standard. A number of major companies are directly involved with TC39, with members representing Microsoft, Google, Apple, Intel, Mozilla, eBay, and more. Some are connected to universities, while others participate as individuals. In addition to voting members, many people participate in discussions regarding the various proposals that have been submitted. While the committee itself only meets every two months, these discussions on the proposals and specifications are taking place publicly, and anyone can participate in the conversation. Proposals are hosted on GitHub, and so discussions are as simple as creating an issue or pull request. A TC39 Discourse page is another way for the JavaScript community to discuss any current proposals or new ideas that haven't been formalized yet. When the committee votes to approve a new standard, this change is then implemented in the runtime authors (such as Google's V8). But how does a new standard get added to JavaScript? The Stages of Proposal There are 5 stages to adding a new standard to JavaScript, starting at Stage 0. Each of these stages has different requirements for completion. There is no time limit on moving a proposal from one stage to the next, and no guarantee that a given proposal will be completed. TC39's website hosts a process document that explains in detail what a given stage means, and how a proposal advances to the next stage. Let's walk through the stages, and look at some of the proposals currently at each stage. Stage 0 The first stage for any proposal is stage 0. This stage is the first step in adding a feature to JavaScript. Anyone can make a proposal. You don't have to be a member of TC39. A detailed document outlines the process for submitting a new proposal into stage 0. The pain purpose of this stage is to start a conversation and begin formalizing the proposal in order for future work to be done with it. The first thing that needs to be done when a proposal is stage 0 is to find a champion. A champion is someone from TC39 who will take the lead on moving a proposal forward. In addition, work will need to go into the documentation for the proposal, such as an outline of the problem that is being addressed and a high-level API design. Once these requirements are met, the committee can vote to move the proposal to Stage 1. An interesting Stage 0 proposal is to add a deprecated global or directive to the language, so that it's easier to alert a developer when a given API has been deprecated. Example: ` Stage 1 The purpose of Stage 1 is to make the case for changing the JavaScript standard, describing the proposed solution, and any potential problems that it could cause or could be impacted by. The main goal of the committee for a Stage 1 proposal is to devote time to examining the problem, and ensuring the proposal resolves it. Typically, browser/runtimes won't make any changes to implement a Stage 1 proposal, because the API could still change pretty drastically. However, polyfills or demos may be created in order to get additional feedback on a given API. These features should not be considered production ready. Once the initial spec has been developed, the committee can vote to move the proposal to Stage 2. The pipeline operator is a great example of a Stage 1 proposal. Its goal is to add a pipeline operator (|>) to JavaScript, in order to pipe function returns or values from one function to the next. There has been some discussion around how it should pass arguments into the second function ` Another Stage 1 proposal is the compartments proposal, which helps resolve a number of issues regarding global scope of a JS file or application. Check it out! Stage 2 When a proposal reaches Stage 2, the committee is focused on writing a precise syntax using formal language. This still doesn't mean that a feature is going to make it to JavaScript, but some experimental implementations will start appearing. This process to create a defined syntax could take from months to a year, with some proposals sitting in Stage 2 for much longer than that. However, when a feature leaves Stage 2, it typically means that the proposal will eventually make it to the final spec. Changes may still happen, but typically only limited changes will happen once a proposal moves out of Stage 2. There are a number of interesting proposals in Stage 2 at the moment, including decorators and iterator helpers. Often, proposals may get stalled in Stage 2. Decorators are a good example of that. According to the TC39 proposals repository, Decorators haven't been presented since September 2020, and were originally discussed back in 2018. Sometimes, the problem being solved has multiple solutions, or there are multiple competing solutions that could be adopted. Other times, the problem turns out to be less urgent or important than previously thought. While it can be frustrating to have a proposal stall out, it's important to remember that any change to JavaScript is permanent - no standardized feature in JavaScript is removed from the spec. Better to move slowly than to end up with half-finished APIs that don't actually solve anything. Stage 3 Stage 3 is the final stage for changes to be made to the specification. Spec compliant implementations will start to roll out, typically behind feature flags, in order to get developers to start using the feature and provide feedback. Changes are still possible, but they are expected to be limited in nature. The new Temporal object is a Stage 3 proposal that's pretty exciting for the JS ecosystem. Temporal will act as an upgrade from the Date object and support additional feature such as time zones. A prototype polyfill can be found on NPM, although keep in mind that it doesn't create a global Temporal object like the finished spec would do. And again, remember that this is still a proposal, and should not be treated as production ready. Another great example of a Stage 3 proposal is Realms, which provides a way to create distinct global environments. Stage 4 When a proposal reaches Stage 4, it is considered complete and ready for implementation by the different runtime vendors. Browsers will start to ship the feature, and other runtimes like Node and Deno will also work to include it in upcoming versions. A features is ready for Stage 4 when it passes all the agreed upon tests, and there has been sufficient testing by developers to ensure that the API is sound. Once a feature is in Stage 4, its spec is not intended to be changed. This is to ensure that the web platform is stable into the future - it's important to not break the web with changes to JavaScript. Two good examples of recent Stage 4 proposals are nullish coalescing and Promise.any. These features have been released into major browsers, and are available to be used today in modern JavaScript applications. Conclusion It's pretty amazing that the JavaScript language is developed in the open like this, for all interested parties to add a voice to the discussion. Not every standard or programming language is developed like this. However, this level of openness can also be difficult, especially if a specific feature gets stalled or a proposed API ends up not being accepted. If you submit your own proposal to TC39, remember that you are trying to solve a specific problem, not simply create a feature in JavaScript. Your proposal may be adjusted or replaced as other voices are added to the discussion. Also, keep in mind that it could take a long time for a proposal to make it into the language, if ever (looking at you, decorators). Also, while I've highlighted mostly good things about this process, it's also possible for a single member to hold back a feature from advancing into the next stage. This can be frustrating, but as noted above, it's important for JavaScript to be developed methodically. Having multiple standards or multiple interpretations of those standards wouldn't benefit anyone, after all. At the end of the day, remember that TC39 is made up of indivduals who are invested in the JavaScript ecosystem, and want to work together with developers to improve the language. They have a lot of context and understanding for how features are implemented that developers may not have. Proposals that don't make it into the language may not make it for valid reasons. Does any of this interest you? Do you want to contribute to the discussion? You can find ways to participate on TC39's website, including links to their Github and Discourse....

Understanding Sourcemaps: From Development to Production cover image

Understanding Sourcemaps: From Development to Production

What Are Sourcemaps? Modern web development involves transforming your source code before deploying it. We minify JavaScript to reduce file sizes, bundle multiple files together, transpile TypeScript to JavaScript, and convert modern syntax into browser-compatible code. These optimizations are essential for performance, but they create a significant problem: the code running in production does not look like the original code you wrote. Here's a simple example. Your original code might look like this: ` After minification, it becomes something like this: ` Now imagine trying to debug an error in that minified code. Which line threw the exception? What was the value of variable d? This is where sourcemaps come in. A sourcemap is a JSON file that contains a mapping between your transformed code and your original source files. When you open browser DevTools, the browser reads these mappings and reconstructs your original code, allowing you to debug with variable names, comments, and proper formatting intact. How Sourcemaps Work When you build your application with tools like Webpack, Vite, or Rollup, they can generate sourcemap files alongside your production bundles. A minified file references its sourcemap using a special comment at the end: ` The sourcemap file itself contains a JSON structure with several key fields: ` The mappings field uses an encoding format called VLQ (Variable Length Quantity) to map each position in the minified code back to its original location. The browser's DevTools use this information to show you the original code while you're debugging. Types of Sourcemaps Build tools support several variations of sourcemaps, each with different trade-offs: Inline sourcemaps: The entire mapping is embedded directly in your JavaScript file as a base64 encoded data URL. This increases file size significantly but simplifies deployment during development. ` External sourcemaps: A separate .map file that's referenced by the JavaScript bundle. This is the most common approach, as it keeps your production bundles lean since sourcemaps are only downloaded when DevTools is open. Hidden sourcemaps: External sourcemap files without any reference in the JavaScript bundle. These are useful when you want sourcemaps available for error tracking services like Sentry, but don't want to expose them to end users. Why Sourcemaps During development, sourcemaps are absolutely critical. They will help avoid having to guess where errors occur, making debugging much easier. Most modern build tools enable sourcemaps by default in development mode. Sourcemaps in Production Should you ship sourcemaps to production? It depends. While security by making your code more difficult to read is not real security, there's a legitimate argument that exposing your source code makes it easier for attackers to understand your application's internals. Sourcemaps can reveal internal API endpoints and routing logic, business logic, and algorithmic implementations, code comments that might contain developer notes or TODO items. Anyone with basic developer tools can reconstruct your entire codebase when sourcemaps are publicly accessible. While the Apple leak contained no credentials or secrets, it did expose their component architecture and implementation patterns. Additionally, code comments can inadvertently contain internal URLs, developer names, or company-specific information that could potentially be exploited by attackers. But that’s not all of it. On the other hand, services like Sentry can provide much more actionable error reports when they have access to sourcemaps. So you can understand exactly where errors happened. If a customer reports an issue, being able to see the actual error with proper context makes diagnosis significantly faster. If your security depends on keeping your frontend code secret, you have bigger problems. Any determined attacker can reverse engineer minified JavaScript. It just takes more time. Sourcemaps are only downloaded when DevTools is open, so shipping them to production doesn't affect load times or performance for end users. How to manage sourcemaps in production You don't have to choose between no sourcemaps and publicly accessible ones. For example, you can restrict access to sourcemaps with server configuration. You can make .map accessible from specific IP addresses. Additionally, tools like Sentry allow you to upload sourcemaps during your build process without making them publicly accessible. Then configure your build to generate sourcemaps without the reference comment, or use hidden sourcemaps. Sentry gets the mapping information it needs, but end users can't access the files. Learning from Apple's Incident Apple's sourcemap incident is a valuable reminder that even the largest tech companies can make deployment oversights. But it also highlights something important: the presence of sourcemaps wasn't actually a security vulnerability. This can be achieved by following good security practices. Never include sensitive data in client code. Developers got an interesting look at how Apple structures its Svelte codebase. The lesson is that you must be intentional about your deployment configuration. If you're going to include sourcemaps in production, make that decision deliberately after considering the trade-offs. And if you decide against using public sourcemaps, verify that your build process actually removes them. In this case, the public repo was quickly removed after Apple filed a DMCA takedown. (https://github.com/github/dmca/blob/master/2025/11/2025-11-05-apple.md) Making the Right Choice So what should you do with sourcemaps in your projects? For development: Always enable them. Use fast options, such as eval-source-map in Webpack or the default configuration in Vite. The debugging benefits far outweigh any downsides. For production: Consider your specific situation. But most importantly, make sure your sourcemaps don't accidentally expose secrets. Review your build output, check for hardcoded credentials, and ensure sensitive configurations stay on the backend where they belong. Conclusion Sourcemaps are powerful development tools that bridge the gap between the optimized code your users download and the readable code you write. They're essential for debugging and make error tracking more effective. The question of whether to include them in production doesn't have a unique answer. Whatever you decide, make it a deliberate choice. Review your build configuration. Verify that sourcemaps are handled the way you expect. And remember that proper frontend security doesn't come from hiding your code. Useful Resources * Source map specification - https://tc39.es/ecma426/ * What are sourcemaps - https://web.dev/articles/source-maps * VLQ implementation - https://github.com/Rich-Harris/vlq * Sentry sourcemaps - https://docs.sentry.io/platforms/javascript/sourcemaps/ * Apple DMCA takedown - https://github.com/github/dmca/blob/master/2025/11/2025-11-05-apple.md...

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co